create-nx-workspace 23.1.0-beta.5 → 23.1.0-beta.7
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/dist/bin/create-nx-workspace.d.ts +2 -0
- package/dist/bin/create-nx-workspace.js +33 -11
- package/dist/src/create-empty-workspace.js +5 -2
- package/dist/src/create-workspace-options.d.ts +6 -0
- package/dist/src/create-workspace.js +10 -4
- package/dist/src/internal-utils/prompts.js +0 -4
- package/dist/src/utils/ai/ai-output.d.ts +1 -1
- package/dist/src/utils/git/git.d.ts +0 -5
- package/dist/src/utils/git/git.js +5 -17
- package/dist/src/utils/nx/ab-testing.d.ts +4 -4
- package/dist/src/utils/nx/ab-testing.js +5 -5
- package/dist/src/utils/template/download-template.d.ts +11 -0
- package/dist/src/utils/template/download-template.js +112 -0
- package/package.json +2 -1
- package/dist/src/utils/template/clone-template.d.ts +0 -1
- package/dist/src/utils/template/clone-template.js +0 -25
|
@@ -65,6 +65,8 @@ interface UnknownStackArguments extends BaseArguments {
|
|
|
65
65
|
}
|
|
66
66
|
type Arguments = NoneArguments | ReactArguments | AngularArguments | VueArguments | NodeArguments | UnknownStackArguments;
|
|
67
67
|
export declare const commandsObject: yargs.Argv<Arguments>;
|
|
68
|
+
/** Workspace names must start with a letter. */
|
|
69
|
+
export declare function isValidWorkspaceName(name: string): boolean;
|
|
68
70
|
export declare function validateWorkspaceName(name: string): void;
|
|
69
71
|
/**
|
|
70
72
|
* Resolves special folder name patterns (`.`, `./`, absolute paths) into a
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.commandsObject = void 0;
|
|
4
|
+
exports.isValidWorkspaceName = isValidWorkspaceName;
|
|
4
5
|
exports.validateWorkspaceName = validateWorkspaceName;
|
|
5
6
|
exports.resolveSpecialFolderName = resolveSpecialFolderName;
|
|
6
7
|
exports.determineFolder = determineFolder;
|
|
@@ -620,9 +621,12 @@ function invariant(predicate, errorCode, message) {
|
|
|
620
621
|
throw new error_utils_1.CnwError(errorCode, message);
|
|
621
622
|
}
|
|
622
623
|
}
|
|
624
|
+
/** Workspace names must start with a letter. */
|
|
625
|
+
function isValidWorkspaceName(name) {
|
|
626
|
+
return /^[a-zA-Z]/.test(name);
|
|
627
|
+
}
|
|
623
628
|
function validateWorkspaceName(name) {
|
|
624
|
-
|
|
625
|
-
if (!pattern.test(name)) {
|
|
629
|
+
if (!isValidWorkspaceName(name)) {
|
|
626
630
|
throw new error_utils_1.CnwError('INVALID_WORKSPACE_NAME', `The workspace name "${name}" is invalid. Workspace names must start with a letter. Examples of valid names: myapp, MyApp, my-app, my_app`);
|
|
627
631
|
}
|
|
628
632
|
}
|
|
@@ -641,12 +645,9 @@ function isCurrentDirReference(folderName) {
|
|
|
641
645
|
* input is a regular name that needs no special handling.
|
|
642
646
|
*/
|
|
643
647
|
function resolveSpecialFolderName(folderName) {
|
|
644
|
-
// User wants to
|
|
648
|
+
// User wants to scaffold in the current directory.
|
|
645
649
|
if (isCurrentDirReference(folderName)) {
|
|
646
650
|
const cwd = (0, path_1.resolve)(process.cwd());
|
|
647
|
-
if ((0, fs_1.readdirSync)(cwd).length > 0) {
|
|
648
|
-
throw new error_utils_1.CnwError('DIRECTORY_EXISTS', `The current directory is not empty. Use "nx init" to add Nx to an existing project.`);
|
|
649
|
-
}
|
|
650
651
|
return { name: (0, path_1.basename)(cwd), workingDir: (0, path_1.dirname)(cwd) };
|
|
651
652
|
}
|
|
652
653
|
// Handle absolute paths like /tmp/acme
|
|
@@ -677,11 +678,20 @@ async function determineFolder(parsedArgs) {
|
|
|
677
678
|
parsedArgs.workingDir = resolved.workingDir;
|
|
678
679
|
}
|
|
679
680
|
validateWorkspaceName(folderName);
|
|
680
|
-
// When input is "." or "./",
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
// the workspace name to the directory name.
|
|
681
|
+
// When input is "." or "./", scaffold into the current directory. The
|
|
682
|
+
// target always "exists" because it IS the cwd, so skip the existsSync
|
|
683
|
+
// check and use the directory name as the workspace name.
|
|
684
684
|
if (isCurrentDirReference(rawFolderName)) {
|
|
685
|
+
// Interactively confirm before scaffolding into the current directory;
|
|
686
|
+
// non-interactive (CI/AI) proceeds without prompting.
|
|
687
|
+
if (parsedArgs.interactive && !(0, is_ci_1.isCI)()) {
|
|
688
|
+
if (!(await promptCreateInCurrentDir(folderName))) {
|
|
689
|
+
// Declined - fall back to creating a named subfolder under the cwd.
|
|
690
|
+
parsedArgs.workingDir = undefined;
|
|
691
|
+
return promptForFolder(parsedArgs);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
parsedArgs.useCurrentDir = true;
|
|
685
695
|
return folderName;
|
|
686
696
|
}
|
|
687
697
|
// If directory exists, either re-prompt (interactive) or error (non-interactive)
|
|
@@ -710,6 +720,18 @@ async function determineFolder(parsedArgs) {
|
|
|
710
720
|
}
|
|
711
721
|
return promptForFolder(parsedArgs);
|
|
712
722
|
}
|
|
723
|
+
async function promptCreateInCurrentDir(dirName) {
|
|
724
|
+
const { useCurrentDir } = await enquirer_1.default.prompt([
|
|
725
|
+
{
|
|
726
|
+
name: 'useCurrentDir',
|
|
727
|
+
message: `Create workspace in the current directory (${dirName})? Existing files may be overwritten.`,
|
|
728
|
+
type: 'autocomplete',
|
|
729
|
+
choices: [{ name: 'Yes' }, { name: 'No' }],
|
|
730
|
+
initial: 0,
|
|
731
|
+
},
|
|
732
|
+
]);
|
|
733
|
+
return useCurrentDir === 'Yes';
|
|
734
|
+
}
|
|
713
735
|
async function promptForFolder(parsedArgs) {
|
|
714
736
|
const reply = await enquirer_1.default.prompt([
|
|
715
737
|
{
|
|
@@ -722,7 +744,7 @@ async function promptForFolder(parsedArgs) {
|
|
|
722
744
|
if (!value) {
|
|
723
745
|
return 'Folder name cannot be empty';
|
|
724
746
|
}
|
|
725
|
-
if (
|
|
747
|
+
if (!isValidWorkspaceName(value)) {
|
|
726
748
|
return 'Workspace name must start with a letter';
|
|
727
749
|
}
|
|
728
750
|
if ((0, fs_1.existsSync)(value)) {
|
|
@@ -27,10 +27,13 @@ async function createEmptyWorkspace(tmpDir, name, packageManager, options) {
|
|
|
27
27
|
// Even though --skipInstall is not an option to create-nx-workspace, we pass through extra options to presets.
|
|
28
28
|
// See: https://github.com/nrwl/nx/issues/31834
|
|
29
29
|
delete options.skipInstall;
|
|
30
|
-
// workingDir
|
|
31
|
-
const { workingDir: _workingDir, ...nxNewOptions } = options;
|
|
30
|
+
// workingDir and useCurrentDir are consumed by CNW itself, not passed to `nx new`
|
|
31
|
+
const { workingDir: _workingDir, useCurrentDir, ...nxNewOptions } = options;
|
|
32
32
|
const args = (0, unparse_1.unparse)({
|
|
33
33
|
...nxNewOptions,
|
|
34
|
+
// Scaffolding into the current directory: relax the generator's
|
|
35
|
+
// empty-directory guard so it can write into a non-empty cwd.
|
|
36
|
+
...(useCurrentDir ? { skipEmptyDirCheck: true } : {}),
|
|
34
37
|
}).join(' ');
|
|
35
38
|
const pmc = (0, package_manager_1.getPackageManagerCommand)(packageManager);
|
|
36
39
|
const command = `new ${args}`;
|
|
@@ -9,6 +9,12 @@ export interface CreateWorkspaceOptions {
|
|
|
9
9
|
* Used when the user provides "." or an absolute path as the workspace name.
|
|
10
10
|
*/
|
|
11
11
|
workingDir?: string;
|
|
12
|
+
/**
|
|
13
|
+
* @description Scaffold into the current directory in place. Set when the
|
|
14
|
+
* user passes "." / "./". Relaxes the empty-directory guard, so existing
|
|
15
|
+
* files in the cwd that collide with generated files are overwritten.
|
|
16
|
+
*/
|
|
17
|
+
useCurrentDir?: boolean;
|
|
12
18
|
packageManager: PackageManager;
|
|
13
19
|
nxCloud: NxCloud;
|
|
14
20
|
useGitHub?: boolean;
|
|
@@ -16,7 +16,7 @@ const nx_cloud_1 = require("./utils/nx/nx-cloud");
|
|
|
16
16
|
const output_1 = require("./utils/output");
|
|
17
17
|
const get_third_party_preset_1 = require("./utils/preset/get-third-party-preset");
|
|
18
18
|
const preset_1 = require("./utils/preset/preset");
|
|
19
|
-
const
|
|
19
|
+
const download_template_1 = require("./utils/template/download-template");
|
|
20
20
|
const update_readme_1 = require("./utils/template/update-readme");
|
|
21
21
|
const child_process_utils_1 = require("./utils/child-process-utils");
|
|
22
22
|
const package_manager_1 = require("./utils/package-manager");
|
|
@@ -40,21 +40,27 @@ async function createWorkspace(preset, options, rawArgs) {
|
|
|
40
40
|
options.template = resolveTemplateShorthand(options.template);
|
|
41
41
|
if (!options.template.startsWith('nrwl/'))
|
|
42
42
|
throw new Error(`Invalid template. Only templates from the 'nrwl' GitHub org are supported.`);
|
|
43
|
-
const templateUrl = `https://github.com/${options.template}`;
|
|
44
43
|
const workingDir = (options.workingDir ?? process.cwd()).replace(/\\/g, '/');
|
|
45
44
|
directory = (0, path_1.join)(workingDir, name);
|
|
45
|
+
// downloadTemplate extracts into `directory`, creating it and overwriting
|
|
46
|
+
// files. That is intended only when scaffolding into the current directory.
|
|
47
|
+
// Otherwise refuse to write over an existing path (the CLI already guards
|
|
48
|
+
// this in determineFolder; this protects direct createWorkspace() callers).
|
|
49
|
+
if (!options.useCurrentDir && (0, node_fs_1.existsSync)(directory)) {
|
|
50
|
+
throw new error_utils_2.CnwError('DIRECTORY_EXISTS', `The directory '${directory}' already exists. Choose a different name or remove the existing directory.`);
|
|
51
|
+
}
|
|
46
52
|
const aiMode = (0, ai_output_1.isAiAgent)();
|
|
47
53
|
// Use spinner for human mode, progress logs for AI mode
|
|
48
54
|
let workspaceSetupSpinner;
|
|
49
55
|
if (aiMode) {
|
|
50
|
-
(0, ai_output_1.logProgress)('
|
|
56
|
+
(0, ai_output_1.logProgress)('downloading', `Downloading template ${options.template}...`);
|
|
51
57
|
}
|
|
52
58
|
else {
|
|
53
59
|
const ora = require('ora');
|
|
54
60
|
workspaceSetupSpinner = ora(`Creating workspace from template`).start();
|
|
55
61
|
}
|
|
56
62
|
try {
|
|
57
|
-
await (0,
|
|
63
|
+
await (0, download_template_1.downloadTemplate)(options.template, directory);
|
|
58
64
|
// Remove npm lockfile from template since we'll generate the correct one
|
|
59
65
|
const npmLockPath = (0, path_1.join)(directory, 'package-lock.json');
|
|
60
66
|
if ((0, node_fs_1.existsSync)(npmLockPath)) {
|
|
@@ -14,7 +14,6 @@ const enquirer_1 = tslib_1.__importDefault(require("enquirer"));
|
|
|
14
14
|
const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
|
15
15
|
const ab_testing_1 = require("../utils/nx/ab-testing");
|
|
16
16
|
const default_base_1 = require("../utils/git/default-base");
|
|
17
|
-
const git_1 = require("../utils/git/git");
|
|
18
17
|
const package_manager_1 = require("../utils/package-manager");
|
|
19
18
|
const string_utils_1 = require("../utils/string-utils");
|
|
20
19
|
const is_ci_1 = require("../utils/ci/is-ci");
|
|
@@ -102,9 +101,6 @@ async function determineTemplate(parsedArgs) {
|
|
|
102
101
|
// Docs generation needs preset flow to document all presets
|
|
103
102
|
if (process.env.NX_GENERATE_DOCS_PROCESS === 'true')
|
|
104
103
|
return 'custom';
|
|
105
|
-
// Template flow requires git for cloning - fall back to custom preset if git is not available
|
|
106
|
-
if (!(0, git_1.isGitAvailable)())
|
|
107
|
-
return 'custom';
|
|
108
104
|
const { template } = await enquirer_1.default.prompt([
|
|
109
105
|
{
|
|
110
106
|
name: 'template',
|
|
@@ -16,7 +16,7 @@ export declare function isReplitAi(): boolean;
|
|
|
16
16
|
export declare function isCursorAi(): boolean;
|
|
17
17
|
export declare function isGeminiCli(): boolean;
|
|
18
18
|
export declare function detectAiAgentName(): string | null;
|
|
19
|
-
export type ProgressStage = 'starting' | '
|
|
19
|
+
export type ProgressStage = 'starting' | 'downloading' | 'installing' | 'configuring' | 'initializing' | 'complete' | 'error';
|
|
20
20
|
export interface ProgressMessage {
|
|
21
21
|
stage: ProgressStage;
|
|
22
22
|
message: string;
|
|
@@ -9,11 +9,6 @@ export declare class GitHubPushError extends Error {
|
|
|
9
9
|
constructor(message: string, reason: 'gh-not-installed' | 'gh-auth-failed' | 'push-timeout' | 'push-failed' | 'env-skip');
|
|
10
10
|
}
|
|
11
11
|
export declare function checkGitVersion(): Promise<string | null | undefined>;
|
|
12
|
-
/**
|
|
13
|
-
* Synchronously checks if git is available on the system.
|
|
14
|
-
* Returns true if git command can be executed, false otherwise.
|
|
15
|
-
*/
|
|
16
|
-
export declare function isGitAvailable(): boolean;
|
|
17
12
|
/**
|
|
18
13
|
* Synchronously checks if GitHub CLI (gh) is available on the system.
|
|
19
14
|
* Returns true if gh command can be executed within 2 seconds, false otherwise.
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.GitHubPushError = exports.VcsPushStatus = void 0;
|
|
4
4
|
exports.checkGitVersion = checkGitVersion;
|
|
5
|
-
exports.isGitAvailable = isGitAvailable;
|
|
6
5
|
exports.isGhCliAvailable = isGhCliAvailable;
|
|
7
6
|
exports.initializeGitRepo = initializeGitRepo;
|
|
8
7
|
exports.pushToGitHub = pushToGitHub;
|
|
@@ -37,19 +36,6 @@ async function checkGitVersion() {
|
|
|
37
36
|
return null;
|
|
38
37
|
}
|
|
39
38
|
}
|
|
40
|
-
/**
|
|
41
|
-
* Synchronously checks if git is available on the system.
|
|
42
|
-
* Returns true if git command can be executed, false otherwise.
|
|
43
|
-
*/
|
|
44
|
-
function isGitAvailable() {
|
|
45
|
-
try {
|
|
46
|
-
(0, child_process_1.execSync)('git --version', { stdio: 'ignore', windowsHide: true });
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
39
|
// 1 second timeout for gh CLI pre-flight checks (version, auth). If gh is
|
|
54
40
|
// wrapped by 1Password, a credential manager, or corporate SSO the call can
|
|
55
41
|
// hang indefinitely. Better to skip the push than freeze the CLI.
|
|
@@ -155,9 +141,11 @@ async function initializeGitRepo(directory, options) {
|
|
|
155
141
|
}
|
|
156
142
|
const insideRepo = await (0, child_process_utils_1.execAndWait)('git rev-parse --is-inside-work-tree', directory, true).then(() => true, () => false);
|
|
157
143
|
if (insideRepo) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
144
|
+
if (process.env.NX_VERBOSE_LOGGING === 'true') {
|
|
145
|
+
output_1.output.log({
|
|
146
|
+
title: 'Directory is already under version control. Skipping initialization of git.',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
161
149
|
return;
|
|
162
150
|
}
|
|
163
151
|
const defaultBase = options.defaultBase || (0, default_base_1.deduceDefaultBase)();
|
|
@@ -2,10 +2,10 @@ import type { BannerVariant, CompletionMessageKey } from './messages';
|
|
|
2
2
|
export declare const NX_CLOUD_URL = "https://nx.dev/nx-cloud";
|
|
3
3
|
/**
|
|
4
4
|
* Clickable Nx Cloud marketing link for cloud prompt footers. Every
|
|
5
|
-
* create-nx-workspace prompt reports the same
|
|
6
|
-
* baked constant embedded directly in the footers. Visible text stays
|
|
7
|
-
* `NX_CLOUD_URL` while clicks carry UTM attribution; terminals
|
|
8
|
-
* support just render the bare URL (CLOUD-4642).
|
|
5
|
+
* create-nx-workspace prompt reports the same content tag, so the link is a
|
|
6
|
+
* single baked constant embedded directly in the footers. Visible text stays
|
|
7
|
+
* the clean `NX_CLOUD_URL` while clicks carry UTM attribution; terminals
|
|
8
|
+
* without OSC 8 support just render the bare URL (CLOUD-4642).
|
|
9
9
|
*/
|
|
10
10
|
export declare const NX_CLOUD_HYPERLINK: string;
|
|
11
11
|
/**
|
|
@@ -17,12 +17,12 @@ const terminal_link_1 = require("../terminal-link");
|
|
|
17
17
|
exports.NX_CLOUD_URL = 'https://nx.dev/nx-cloud';
|
|
18
18
|
/**
|
|
19
19
|
* Clickable Nx Cloud marketing link for cloud prompt footers. Every
|
|
20
|
-
* create-nx-workspace prompt reports the same
|
|
21
|
-
* baked constant embedded directly in the footers. Visible text stays
|
|
22
|
-
* `NX_CLOUD_URL` while clicks carry UTM attribution; terminals
|
|
23
|
-
* support just render the bare URL (CLOUD-4642).
|
|
20
|
+
* create-nx-workspace prompt reports the same content tag, so the link is a
|
|
21
|
+
* single baked constant embedded directly in the footers. Visible text stays
|
|
22
|
+
* the clean `NX_CLOUD_URL` while clicks carry UTM attribution; terminals
|
|
23
|
+
* without OSC 8 support just render the bare URL (CLOUD-4642).
|
|
24
24
|
*/
|
|
25
|
-
exports.NX_CLOUD_HYPERLINK = (0, terminal_link_1.terminalLink)(exports.NX_CLOUD_URL, `${exports.NX_CLOUD_URL}?utm_source=nx-cli&utm_medium=create-nx-workspace`);
|
|
25
|
+
exports.NX_CLOUD_HYPERLINK = (0, terminal_link_1.terminalLink)(exports.NX_CLOUD_URL, `${exports.NX_CLOUD_URL}?utm_source=nx-cli&utm_medium=cli&utm_campaign=nx-cloud-connect&utm_content=create-nx-workspace`);
|
|
26
26
|
// Flow variant controls both tracking and banner display (CLOUD-4235)
|
|
27
27
|
// Variants: 0 = control, 1 = updated prompt, 2 = no prompt (auto-connect)
|
|
28
28
|
const FLOW_VARIANT_CACHE_FILE = (0, node_path_1.join)((0, node_os_1.tmpdir)(), 'nx-cnw-flow-variant');
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download an nrwl template repository and extract it into `directory`.
|
|
3
|
+
*
|
|
4
|
+
* git is not required (fresh machines / CI / AI agents may have none), and an
|
|
5
|
+
* existing `.git` in `directory` is left intact, so this can scaffold into the
|
|
6
|
+
* current directory. Existing files (e.g. README) are overwritten.
|
|
7
|
+
*
|
|
8
|
+
* @param template GitHub repo slug, e.g. `nrwl/react-template`.
|
|
9
|
+
* @param directory Absolute path to extract into.
|
|
10
|
+
*/
|
|
11
|
+
export declare function downloadTemplate(template: string, directory: string): Promise<void>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.downloadTemplate = downloadTemplate;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const node_stream_1 = require("node:stream");
|
|
8
|
+
const promises_1 = require("node:stream/promises");
|
|
9
|
+
const node_zlib_1 = require("node:zlib");
|
|
10
|
+
const tar = tslib_1.__importStar(require("tar-stream"));
|
|
11
|
+
const error_utils_1 = require("../error-utils");
|
|
12
|
+
// Branches to try, in order. GitHub serves a gzipped tarball of a repo at
|
|
13
|
+
// https://github.com/<org>/<repo>/archive/refs/heads/<branch>.tar.gz.
|
|
14
|
+
const DEFAULT_BRANCHES = ['main', 'master'];
|
|
15
|
+
/**
|
|
16
|
+
* Download an nrwl template repository and extract it into `directory`.
|
|
17
|
+
*
|
|
18
|
+
* git is not required (fresh machines / CI / AI agents may have none), and an
|
|
19
|
+
* existing `.git` in `directory` is left intact, so this can scaffold into the
|
|
20
|
+
* current directory. Existing files (e.g. README) are overwritten.
|
|
21
|
+
*
|
|
22
|
+
* @param template GitHub repo slug, e.g. `nrwl/react-template`.
|
|
23
|
+
* @param directory Absolute path to extract into.
|
|
24
|
+
*/
|
|
25
|
+
async function downloadTemplate(template, directory) {
|
|
26
|
+
let body;
|
|
27
|
+
const attempts = [];
|
|
28
|
+
// A thrown fetch is a connectivity problem; a non-ok response is a missing
|
|
29
|
+
// branch/repo. Distinguish them so the error code (and its hints) are right.
|
|
30
|
+
let networkError = false;
|
|
31
|
+
for (const branch of DEFAULT_BRANCHES) {
|
|
32
|
+
const url = `https://github.com/${template}/archive/refs/heads/${branch}.tar.gz`;
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(url);
|
|
35
|
+
if (res.ok && res.body) {
|
|
36
|
+
body = res.body;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
attempts.push(`${branch}: HTTP ${res.status}`);
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
networkError = true;
|
|
43
|
+
attempts.push(`${branch}: ${e instanceof Error ? e.message : String(e)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!body) {
|
|
47
|
+
throw new error_utils_1.CnwError(networkError ? 'NETWORK_ERROR' : 'TEMPLATE_CLONE_FAILED', `Failed to download template '${template}' (${attempts.join('; ')})`);
|
|
48
|
+
}
|
|
49
|
+
// Only remove the directory on failure if we created it - never delete a
|
|
50
|
+
// pre-existing dir (e.g. the user's current directory).
|
|
51
|
+
const dirPreexisted = (0, node_fs_1.existsSync)(directory);
|
|
52
|
+
(0, node_fs_1.mkdirSync)(directory, { recursive: true });
|
|
53
|
+
try {
|
|
54
|
+
await extractTarball(body, directory);
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
if (!dirPreexisted) {
|
|
58
|
+
(0, node_fs_1.rmSync)(directory, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
61
|
+
throw new error_utils_1.CnwError('TEMPLATE_CLONE_FAILED', `Failed to create starter workspace: ${message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function extractTarball(body, directory) {
|
|
65
|
+
const extract = tar.extract();
|
|
66
|
+
extract.on('entry', (header, stream, next) => {
|
|
67
|
+
// Drain the entry and move to the next one without writing anything.
|
|
68
|
+
const skip = () => {
|
|
69
|
+
stream.on('end', next);
|
|
70
|
+
stream.resume();
|
|
71
|
+
};
|
|
72
|
+
try {
|
|
73
|
+
// GitHub wraps everything in a top-level `<repo>-<branch>/` directory.
|
|
74
|
+
// Strip that first segment so files land directly in `directory`.
|
|
75
|
+
const relativePath = header.name.split('/').slice(1).join('/');
|
|
76
|
+
// Top-level dir entry or unsupported type (symlink, pax header).
|
|
77
|
+
if (!relativePath ||
|
|
78
|
+
(header.type !== 'file' && header.type !== 'directory')) {
|
|
79
|
+
return skip();
|
|
80
|
+
}
|
|
81
|
+
const destPath = (0, node_path_1.join)(directory, relativePath);
|
|
82
|
+
// Defense-in-depth against a malicious tarball escaping the target dir
|
|
83
|
+
// (zip-slip) via `..` entries.
|
|
84
|
+
const rel = (0, node_path_1.relative)(directory, destPath);
|
|
85
|
+
if (rel.startsWith('..') || (0, node_path_1.isAbsolute)(rel)) {
|
|
86
|
+
return skip();
|
|
87
|
+
}
|
|
88
|
+
if (header.type === 'directory') {
|
|
89
|
+
(0, node_fs_1.mkdirSync)(destPath, { recursive: true });
|
|
90
|
+
return skip();
|
|
91
|
+
}
|
|
92
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(destPath), { recursive: true });
|
|
93
|
+
const writeStream = (0, node_fs_1.createWriteStream)(destPath, { mode: header.mode });
|
|
94
|
+
// Surface a write failure to the pipeline below so it rejects and tears
|
|
95
|
+
// down every stream.
|
|
96
|
+
writeStream.on('error', (err) => extract.destroy(err));
|
|
97
|
+
writeStream.on('close', next);
|
|
98
|
+
stream.pipe(writeStream);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
// A synchronous failure here (e.g. mkdirSync hitting EACCES/ENOSPC, or a
|
|
102
|
+
// path component that is a file) would otherwise escape the awaited
|
|
103
|
+
// pipeline; route it so the operation rejects instead of crashing.
|
|
104
|
+
extract.destroy(err);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
// pipeline (unlike a manual .pipe() chain) forwards errors from every stage -
|
|
108
|
+
// a network drop on the source or a corrupt/truncated gzip rejects here and
|
|
109
|
+
// destroys all streams, so the caller can wrap it in a CnwError.
|
|
110
|
+
// Cast: the global (DOM) ReadableStream and node:stream/web differ structurally.
|
|
111
|
+
await (0, promises_1.pipeline)(node_stream_1.Readable.fromWeb(body), (0, node_zlib_1.createGunzip)(), extract);
|
|
112
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-nx-workspace",
|
|
3
|
-
"version": "23.1.0-beta.
|
|
3
|
+
"version": "23.1.0-beta.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Smart Monorepos · Fast Builds",
|
|
6
6
|
"repository": {
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"flat": "^5.0.2",
|
|
44
44
|
"open": "^8.4.0",
|
|
45
45
|
"ora": "^5.3.0",
|
|
46
|
+
"tar-stream": "~2.2.0",
|
|
46
47
|
"tmp": "~0.2.7",
|
|
47
48
|
"tslib": "^2.3.0",
|
|
48
49
|
"yargs": "^17.6.2"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function cloneTemplate(templateUrl: string, targetDirectory: string, workingDir?: string): Promise<void>;
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.cloneTemplate = cloneTemplate;
|
|
4
|
-
const child_process_utils_1 = require("../child-process-utils");
|
|
5
|
-
const fs_1 = require("fs");
|
|
6
|
-
const promises_1 = require("fs/promises");
|
|
7
|
-
const path_1 = require("path");
|
|
8
|
-
const error_utils_1 = require("../error-utils");
|
|
9
|
-
async function cloneTemplate(templateUrl, targetDirectory, workingDir) {
|
|
10
|
-
if ((0, fs_1.existsSync)(targetDirectory)) {
|
|
11
|
-
throw new error_utils_1.CnwError('DIRECTORY_EXISTS', `The directory '${targetDirectory}' already exists. Choose a different name or remove the existing directory.`);
|
|
12
|
-
}
|
|
13
|
-
try {
|
|
14
|
-
await (0, child_process_utils_1.execAndWait)(`git clone --depth 1 "${templateUrl}" "${targetDirectory}"`, workingDir ?? process.cwd());
|
|
15
|
-
// Ensure clean history
|
|
16
|
-
const gitDir = (0, path_1.join)(targetDirectory, '.git');
|
|
17
|
-
if ((0, fs_1.existsSync)(gitDir)) {
|
|
18
|
-
await (0, promises_1.rm)(gitDir, { recursive: true, force: true });
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
catch (e) {
|
|
22
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
23
|
-
throw new error_utils_1.CnwError('TEMPLATE_CLONE_FAILED', `Failed to create starter workspace: ${message}`);
|
|
24
|
-
}
|
|
25
|
-
}
|