diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd52e35d8e..6c7596c8f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,7 +64,7 @@ yarn local-registry disable To publish packages to a local registry, do the following: - Run `yarn local-registry start` in Terminal 1 (keep it running) -- Run `npm adduser` in Terminal 2 (real credentials are not required, you just need to be logged in) +- Run `npm adduser --registry http://localhost:4873` in Terminal 2 (real credentials are not required, you just need to be logged in) - Run `yarn local-registry enable` in Terminal 2 - Run `yarn nx-release 999.9.9 --local` in Terminal 2 - Run `cd /tmp` in Terminal 2 diff --git a/packages/create-nx-workspace/bin/create-nx-workspace.ts b/packages/create-nx-workspace/bin/create-nx-workspace.ts index 8a7300797d..41cba686d2 100644 --- a/packages/create-nx-workspace/bin/create-nx-workspace.ts +++ b/packages/create-nx-workspace/bin/create-nx-workspace.ts @@ -1,19 +1,31 @@ #!/usr/bin/env node -import { output } from '@nrwl/workspace/src/utilities/output'; -import { unparse } from '@nrwl/workspace/src/tasks-runner/utils'; -import { Schema, Preset } from '@nrwl/workspace/src/generators/new/new'; -import { - getPackageManagerCommand, - getPackageManagerVersion, -} from '@nrwl/tao/src/shared/package-manager'; import { execSync } from 'child_process'; import { writeFileSync } from 'fs'; import * as inquirer from 'inquirer'; import * as path from 'path'; import { dirSync } from 'tmp'; import * as yargsParser from 'yargs-parser'; -import { showNxWarning } from './shared'; +import { showNxWarning, unparse } from './shared'; +import { output } from './output'; +import { + getPackageManagerCommand, + getPackageManagerVersion, +} from './package-manager'; + +export enum Preset { + Empty = 'empty', + OSS = 'oss', + WebComponents = 'web-components', + Angular = 'angular', + AngularWithNest = 'angular-nest', + React = 'react', + ReactWithExpress = 'react-express', + NextJs = 'next', + Gatsby = 'gatsby', + Nest = 'nest', + Express = 'express', +} const presetOptions: { value: Preset; name: string }[] = [ { @@ -73,12 +85,7 @@ const cliVersion = 'NX_VERSION'; const nxVersion = 'NX_VERSION'; const prettierVersion = 'PRETTIER_VERSION'; -interface WorkspaceArgs extends Schema { - _?: string[]; - help?: boolean; -} - -const parsedArgs: WorkspaceArgs = yargsParser(process.argv.slice(2), { +const parsedArgs: any = yargsParser(process.argv.slice(2), { string: [ 'cli', 'preset', @@ -168,7 +175,7 @@ function showHelp() { `); } -function determineWorkspaceName(parsedArgs: WorkspaceArgs): Promise { +function determineWorkspaceName(parsedArgs: any): Promise { const workspaceName: string = parsedArgs._[0]; if (workspaceName) { @@ -195,7 +202,7 @@ function determineWorkspaceName(parsedArgs: WorkspaceArgs): Promise { }); } -function determinePreset(parsedArgs: WorkspaceArgs): Promise { +function determinePreset(parsedArgs: any): Promise { if (parsedArgs.preset) { if (Object.values(Preset).indexOf(parsedArgs.preset) === -1) { output.error({ @@ -225,10 +232,7 @@ function determinePreset(parsedArgs: WorkspaceArgs): Promise { .then((a: { Preset: Preset }) => a.Preset); } -function determineAppName( - preset: Preset, - parsedArgs: WorkspaceArgs -): Promise { +function determineAppName(preset: Preset, parsedArgs: any): Promise { if (preset === Preset.Empty || preset === Preset.OSS) { return Promise.resolve(''); } @@ -259,7 +263,7 @@ function determineAppName( function determineCli( preset: Preset, - parsedArgs: WorkspaceArgs + parsedArgs: any ): Promise<'nx' | 'angular'> { if (parsedArgs.cli) { if (['nx', 'angular'].indexOf(parsedArgs.cli) === -1) { @@ -283,7 +287,7 @@ function determineCli( } } -function determineStyle(preset: Preset, parsedArgs: WorkspaceArgs) { +function determineStyle(preset: Preset, parsedArgs: any) { if ( preset === Preset.Empty || preset === Preset.OSS || @@ -371,7 +375,7 @@ function determineStyle(preset: Preset, parsedArgs: WorkspaceArgs) { return Promise.resolve(parsedArgs.style); } -function determineLinter(preset: Preset, parsedArgs: WorkspaceArgs) { +function determineLinter(preset: Preset, parsedArgs: any) { if (!parsedArgs.linter) { if (preset === Preset.Angular || preset === Preset.AngularWithNest) { return inquirer @@ -411,7 +415,13 @@ function determineLinter(preset: Preset, parsedArgs: WorkspaceArgs) { } function createSandbox(packageManager: string) { - console.log(`Creating a sandbox with Nx...`); + output.log({ + title: 'Nx is creating your workspace.', + bodyLines: [ + 'To make sure the command works reliably in all environments, and that the preset is applied correctly,', + `Nx will run "${packageManager} install" several times. Please wait.`, + ], + }); const tmpDir = dirSync().name; writeFileSync( path.join(tmpDir, 'package.json'), @@ -434,13 +444,12 @@ function createSandbox(packageManager: string) { return tmpDir; } -function createApp(tmpDir: string, name: string, parsedArgs: WorkspaceArgs) { +function createApp(tmpDir: string, name: string, parsedArgs: any) { const { _, cli, ...restArgs } = parsedArgs; const args = unparse(restArgs).join(' '); const pmc = getPackageManagerCommand(packageManager); const command = `new ${name} ${args} --collection=@nrwl/workspace`; - console.log(command); let nxWorkspaceRoot = `"${process.cwd().replace(/\\/g, '/')}"`; @@ -456,14 +465,27 @@ function createApp(tmpDir: string, name: string, parsedArgs: WorkspaceArgs) { nxWorkspaceRoot = `\\"${nxWorkspaceRoot.slice(1, -1)}\\"`; } } + const fullCommand = `${pmc.exec} tao ${command}/collection.json --cli=${cli} --nxWorkspaceRoot=${nxWorkspaceRoot}`; - execSync( - `${pmc.exec} tao ${command}/collection.json --cli=${cli} --nxWorkspaceRoot=${nxWorkspaceRoot}`, - { + try { + execSync(fullCommand, { + stdio: ['ignore', 'ignore', 'ignore'], + cwd: tmpDir, + }); + } catch (e) { + output.error({ + title: + 'Something went wrong. Rerunning the command with verbose logging.', + }); + execSync(fullCommand, { stdio: [0, 1, 2], cwd: tmpDir, - } - ); + }); + } + + output.success({ + title: 'Nx has successfully created the workspace.', + }); if (parsedArgs.nxCloud) { output.addVerticalSeparator(); @@ -474,7 +496,7 @@ function createApp(tmpDir: string, name: string, parsedArgs: WorkspaceArgs) { } } -async function askAboutNxCloud(parsedArgs: WorkspaceArgs) { +async function askAboutNxCloud(parsedArgs: any) { if (parsedArgs.nxCloud === undefined) { return inquirer .prompt([ diff --git a/packages/create-nx-workspace/bin/output.ts b/packages/create-nx-workspace/bin/output.ts new file mode 100644 index 0000000000..fcf3fdff75 --- /dev/null +++ b/packages/create-nx-workspace/bin/output.ts @@ -0,0 +1,210 @@ +import * as chalk from 'chalk'; +/* + * Because we don't want to depend on @nrwl/workspace (to speed up the workspace creation) + * we duplicate the helper functions from @nrwl/workspace in this file. + */ +export interface CLIErrorMessageConfig { + title: string; + bodyLines?: string[]; + slug?: string; +} + +export interface CLIWarnMessageConfig { + title: string; + bodyLines?: string[]; + slug?: string; +} + +export interface CLINoteMessageConfig { + title: string; + bodyLines?: string[]; +} + +export interface CLISuccessMessageConfig { + title: string; + bodyLines?: string[]; +} + +/** + * Automatically disable styling applied by chalk if CI=true + */ +if (process.env.CI === 'true') { + (chalk as any).level = 0; +} + +class CLIOutput { + private readonly NX_PREFIX = `${chalk.cyan( + '>' + )} ${chalk.reset.inverse.bold.cyan(' NX ')}`; + /** + * Longer dash character which forms more of a continuous line when place side to side + * with itself, unlike the standard dash character + */ + private readonly VERTICAL_SEPARATOR = + '———————————————————————————————————————————————'; + + /** + * Expose some color and other utility functions so that other parts of the codebase that need + * more fine-grained control of message bodies are still using a centralized + * implementation. + */ + colors = { + gray: chalk.gray, + }; + bold = chalk.bold; + underline = chalk.underline; + + private writeToStdOut(str: string) { + process.stdout.write(str); + } + + private writeOutputTitle({ + label, + title, + }: { + label?: string; + title: string; + }): void { + let outputTitle: string; + if (label) { + outputTitle = `${this.NX_PREFIX} ${label} ${title}\n`; + } else { + outputTitle = `${this.NX_PREFIX} ${title}\n`; + } + this.writeToStdOut(outputTitle); + } + + private writeOptionalOutputBody(bodyLines?: string[]): void { + if (!bodyLines) { + return; + } + this.addNewline(); + bodyLines.forEach((bodyLine) => this.writeToStdOut(' ' + bodyLine + '\n')); + } + + addNewline() { + this.writeToStdOut('\n'); + } + + addVerticalSeparator() { + this.writeToStdOut(`\n${chalk.gray(this.VERTICAL_SEPARATOR)}\n\n`); + } + + addVerticalSeparatorWithoutNewLines() { + this.writeToStdOut(`${chalk.gray(this.VERTICAL_SEPARATOR)}\n`); + } + + error({ title, slug, bodyLines }: CLIErrorMessageConfig) { + this.addNewline(); + + this.writeOutputTitle({ + label: chalk.reset.inverse.bold.red(' ERROR '), + title: chalk.bold.red(title), + }); + + this.writeOptionalOutputBody(bodyLines); + + /** + * Optional slug to be used in an Nx error message redirect URL + */ + if (slug && typeof slug === 'string') { + this.addNewline(); + this.writeToStdOut( + chalk.grey(' ' + 'Learn more about this error: ') + + 'https://errors.nx.dev/' + + slug + + '\n' + ); + } + + this.addNewline(); + } + + warn({ title, slug, bodyLines }: CLIWarnMessageConfig) { + this.addNewline(); + + this.writeOutputTitle({ + label: chalk.reset.inverse.bold.yellow(' WARNING '), + title: chalk.bold.yellow(title), + }); + + this.writeOptionalOutputBody(bodyLines); + + /** + * Optional slug to be used in an Nx warning message redirect URL + */ + if (slug && typeof slug === 'string') { + this.addNewline(); + this.writeToStdOut( + chalk.grey(' ' + 'Learn more about this warning: ') + + 'https://errors.nx.dev/' + + slug + + '\n' + ); + } + + this.addNewline(); + } + + note({ title, bodyLines }: CLINoteMessageConfig) { + this.addNewline(); + + this.writeOutputTitle({ + label: chalk.reset.inverse.bold.keyword('orange')(' NOTE '), + title: chalk.bold.keyword('orange')(title), + }); + + this.writeOptionalOutputBody(bodyLines); + + this.addNewline(); + } + + success({ title, bodyLines }: CLISuccessMessageConfig) { + this.addNewline(); + + this.writeOutputTitle({ + label: chalk.reset.inverse.bold.green(' SUCCESS '), + title: chalk.bold.green(title), + }); + + this.writeOptionalOutputBody(bodyLines); + + this.addNewline(); + } + + logSingleLine(message: string) { + this.addNewline(); + + this.writeOutputTitle({ + title: message, + }); + + this.addNewline(); + } + + logCommand(message: string, isCached: boolean = false) { + this.addNewline(); + + this.writeToStdOut(chalk.bold(`> ${message} `)); + + if (isCached) { + this.writeToStdOut(chalk.bold.grey(`[retrieved from cache]`)); + } + + this.addNewline(); + } + + log({ title, bodyLines }: CLIWarnMessageConfig) { + this.addNewline(); + + this.writeOutputTitle({ + title: chalk.white(title), + }); + + this.writeOptionalOutputBody(bodyLines); + + this.addNewline(); + } +} + +export const output = new CLIOutput(); diff --git a/packages/create-nx-workspace/bin/package-manager.ts b/packages/create-nx-workspace/bin/package-manager.ts new file mode 100644 index 0000000000..c39c525982 --- /dev/null +++ b/packages/create-nx-workspace/bin/package-manager.ts @@ -0,0 +1,81 @@ +import { execSync } from 'child_process'; +import { existsSync } from 'fs'; +import { join } from 'path'; + +/* + * Because we don't want to depend on @nrwl/workspace (to speed up the workspace creation) + * we duplicate the helper functions from @nrwl/workspace in this file. + */ + +export function detectPackageManager(dir = '') { + return existsSync(join(dir, 'yarn.lock')) + ? 'yarn' + : existsSync(join(dir, 'pnpm-lock.yaml')) + ? 'pnpm' + : 'npm'; +} + +/** + * Returns commands for the package manager used in the workspace. + * By default, the package manager is derived based on the lock file, + * but it can also be passed in explicitly. + * + * Example: + * + * ```javascript + * execSync(`${getPackageManagerCommand().addDev} my-dev-package`); + * ``` + * + */ +export function getPackageManagerCommand( + packageManager = detectPackageManager() +): { + install: string; + add: string; + addDev: string; + rm: string; + exec: string; + list: string; + run: (script: string, args: string) => string; +} { + switch (packageManager) { + case 'yarn': + return { + install: 'yarn', + add: 'yarn add', + addDev: 'yarn add -D', + rm: 'yarn remove', + exec: 'yarn', + run: (script: string, args: string) => `yarn ${script} ${args}`, + list: 'yarn list', + }; + + case 'pnpm': + return { + install: 'pnpm install --no-frozen-lockfile', // explicitly disable in case of CI + add: 'pnpm add', + addDev: 'pnpm add -D', + rm: 'pnpm rm', + exec: 'pnpx', + run: (script: string, args: string) => `pnpm run ${script} -- ${args}`, + list: 'pnpm ls --depth 100', + }; + + case 'npm': + return { + install: 'npm install --legacy-peer-deps', + add: 'npm install --legacy-peer-deps', + addDev: 'npm install --legacy-peer-deps -D', + rm: 'npm rm', + exec: 'npx', + run: (script: string, args: string) => `npm run ${script} -- ${args}`, + list: 'npm ls', + }; + } +} + +export function getPackageManagerVersion( + packageManager: 'npm' | 'yarn' | 'pnpm' +): string { + return execSync(`${packageManager} --version`).toString('utf-8').trim(); +} diff --git a/packages/create-nx-workspace/bin/shared.ts b/packages/create-nx-workspace/bin/shared.ts index 609ad14156..11333cf1d0 100644 --- a/packages/create-nx-workspace/bin/shared.ts +++ b/packages/create-nx-workspace/bin/shared.ts @@ -1,7 +1,12 @@ import * as path from 'path'; import { execSync } from 'child_process'; -import { output } from '@nrwl/workspace/src/utilities/output'; +import { output } from './output'; +import * as flatten from 'flat'; +/* + * Because we don't want to depend on @nrwl/workspace (to speed up the workspace creation) + * we duplicate the helper functions from @nrwl/workspace in this file. + */ export function showNxWarning(workspaceName: string) { try { const pathToRunNxCommand = path.resolve(process.cwd(), workspaceName); @@ -21,3 +26,36 @@ export function showNxWarning(workspaceName: string) { }); } } + +export function unparse(options: Object): string[] { + const unparsed = []; + for (const key of Object.keys(options)) { + const value = options[key]; + unparseOption(key, value, unparsed); + } + + return unparsed; +} + +function unparseOption(key: string, value: any, unparsed: string[]) { + if (value === true) { + unparsed.push(`--${key}`); + } else if (value === false) { + unparsed.push(`--no-${key}`); + } else if (Array.isArray(value)) { + value.forEach((item) => unparseOption(key, item, unparsed)); + } else if (Object.prototype.toString.call(value) === '[object Object]') { + const flattened = flatten(value, { safe: true }); + for (const flattenedKey in flattened) { + unparseOption( + `${key}.${flattenedKey}`, + flattened[flattenedKey], + unparsed + ); + } + } else if (typeof value === 'string' && value.includes(' ')) { + unparsed.push(`--${key}="${value}"`); + } else if (value != null) { + unparsed.push(`--${key}=${value}`); + } +} diff --git a/packages/create-nx-workspace/package.json b/packages/create-nx-workspace/package.json index 2a0bc0c38a..b1b054c973 100644 --- a/packages/create-nx-workspace/package.json +++ b/packages/create-nx-workspace/package.json @@ -27,11 +27,10 @@ }, "homepage": "https://nx.dev", "dependencies": { - "@nrwl/workspace": "*", "tmp": "0.0.33", "yargs-parser": "20.0.0", - "tslib": "^2.0.0", "inquirer": "^6.3.1", - "typescript": "~4.0.3" + "flat": "^5.0.2", + "chalk": "4.1.0" } } diff --git a/scripts/build-angular.js b/scripts/build-angular.js index da90f5f8a5..8935c06798 100644 --- a/scripts/build-angular.js +++ b/scripts/build-angular.js @@ -1,7 +1,9 @@ const childProcess = require('child_process'); const fs = require('fs-extra'); -childProcess.execSync(`npx ng-packagr -p packages/angular/ng-package.json`); +childProcess.execSync(`npx ng-packagr -p packages/angular/ng-package.json`, { + stdio: [0, 1, 2], +}); fs.removeSync('packages/angular/dist/src'); fs.copySync('packages/angular/dist/', 'build/packages/angular/'); fs.removeSync('packages/angular/dist/');