From 7a3ce2ac10d8e02f14802ec2ea0a8ca9d91ded46 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 15 Feb 2024 12:14:49 +0100 Subject: [PATCH] Better handling of create-docusaurus javascript/typescript option + refactor --- packages/create-docusaurus/src/index.ts | 389 ++++++++++++---------- packages/docusaurus-utils/src/cliUtils.ts | 63 ++-- packages/docusaurus-utils/src/index.ts | 2 +- 3 files changed, 262 insertions(+), 192 deletions(-) diff --git a/packages/create-docusaurus/src/index.ts b/packages/create-docusaurus/src/index.ts index 33959c965b2b..769fb4885213 100755 --- a/packages/create-docusaurus/src/index.ts +++ b/packages/create-docusaurus/src/index.ts @@ -13,17 +13,28 @@ import logger from '@docusaurus/logger'; import shell from 'shelljs'; import prompts, {type Choice} from 'prompts'; import supportsColor from 'supports-color'; -import { - escapeShellArg, - getLanguage, - type LanguagesOptions, -} from '@docusaurus/utils'; +import {escapeShellArg, askPreferredLanguage} from '@docusaurus/utils'; -type CLIOptions = { +type LanguagesOptions = { + javascript?: boolean; + typescript?: boolean; +}; + +type CLIOptions = LanguagesOptions & { packageManager?: PackageManager; skipInstall?: boolean; gitStrategy?: GitStrategy; -} & LanguagesOptions; +}; + +async function getLanguage(options: LanguagesOptions) { + if (options.typescript) { + return 'typescript'; + } + if (options.javascript) { + return 'javascript'; + } + return askPreferredLanguage(); +} // Only used in the rare, rare case of running globally installed create + // using --skip-install. We need a default name to show the tip text @@ -156,11 +167,14 @@ async function readTemplates(): Promise { async function copyTemplate( template: Template, dest: string, - typescript: boolean, + language: 'javascript' | 'typescript', ): Promise { await fs.copy(path.join(templatesDir, 'shared'), dest); - await fs.copy(typescript ? template.tsVariantPath! : template.path, dest, { + const sourcePath = + language === 'typescript' ? template.tsVariantPath! : template.path; + + await fs.copy(sourcePath, dest, { // Symlinks don't exist in published npm packages anymore, so this is only // to prevent errors during local testing filter: async (filePath) => !(await fs.lstat(filePath)).isSymbolicLink(), @@ -186,6 +200,33 @@ function createTemplateChoices(templates: Template[]): Choice[] { ]; } +async function askTemplateChoice({ + templates, + cliOptions, +}: { + templates: Template[]; + cliOptions: CLIOptions; +}) { + return cliOptions.gitStrategy + ? 'Git repository' + : ( + (await prompts( + { + type: 'select', + name: 'template', + message: 'Select a template below...', + choices: createTemplateChoices(templates), + }, + { + onCancel() { + logger.error('A choice is required.'); + process.exit(1); + }, + }, + )) as {template: Template | 'Git repository' | 'Local template'} + ).template; +} + function isValidGitRepoUrl(gitRepoUrl: string): boolean { return ['https://', 'git@'].some((item) => gitRepoUrl.startsWith(item)); } @@ -220,23 +261,6 @@ async function getGitCommand(gitStrategy: GitStrategy): Promise { } } -function getTemplate( - templates: Template[], - reqTemplate?: string, - typescript?: boolean, -) { - const template = templates.find((t) => t.name === reqTemplate); - if (!template) { - logger.error('Invalid template.'); - process.exit(1); - } - if (typescript && !template.tsVariantPath) { - logger.error`Template name=${reqTemplate!} doesn't provide the TypeScript variant.`; - process.exit(1); - } - return template; -} - async function getSiteName( reqName: string | undefined, rootDir: string, @@ -280,7 +304,7 @@ type Source = | { type: 'template'; template: Template; - typescript: boolean; + language: 'javascript' | 'typescript'; } | { type: 'git'; @@ -292,162 +316,193 @@ type Source = path: string; }; -async function getSource( - reqTemplate: string | undefined, - templates: Template[], - language: LanguagesOptions, - cliOptions: CLIOptions, -): Promise { - if (reqTemplate) { - if (isValidGitRepoUrl(reqTemplate)) { - if ( - cliOptions.gitStrategy && - !gitStrategies.includes(cliOptions.gitStrategy) - ) { - logger.error`Invalid git strategy: name=${ - cliOptions.gitStrategy - }. Value must be one of ${gitStrategies.join(', ')}.`; - process.exit(1); - } - return { - type: 'git', - url: reqTemplate, - strategy: cliOptions.gitStrategy ?? 'deep', - }; - } else if (await fs.pathExists(path.resolve(reqTemplate))) { - return { - type: 'local', - path: path.resolve(reqTemplate), - }; - } +async function createTemplateSource({ + template, + cliOptions, +}: { + template: Template; + cliOptions: CLIOptions; +}): Promise { + const language = await getLanguage(cliOptions); + if (language === 'typescript' && !template.tsVariantPath) { + logger.error`Template name=${template.name} doesn't provide a TypeScript variant.`; + process.exit(1); + } + return { + type: 'template', + template, + language, + }; +} - const template = getTemplate(templates, reqTemplate, language.typescript); +async function getTemplateSource({ + templateName, + templates, + cliOptions, +}: { + templateName: string; + templates: Template[]; + cliOptions: CLIOptions; +}): Promise { + const template = templates.find((t) => t.name === templateName); + if (!template) { + logger.error('Invalid template.'); + process.exit(1); + } + return createTemplateSource({template, cliOptions}); +} +// Get the template source explicitly requested by the user provided cli option +async function getUserProvidedSource({ + reqTemplate, + templates, + cliOptions, +}: { + reqTemplate: string; + templates: Template[]; + cliOptions: CLIOptions; +}): Promise { + if (isValidGitRepoUrl(reqTemplate)) { + if ( + cliOptions.gitStrategy && + !gitStrategies.includes(cliOptions.gitStrategy) + ) { + logger.error`Invalid git strategy: name=${ + cliOptions.gitStrategy + }. Value must be one of ${gitStrategies.join(', ')}.`; + process.exit(1); + } return { - type: 'template', - template, - typescript: language.typescript ?? false, + type: 'git', + url: reqTemplate, + strategy: cliOptions.gitStrategy ?? 'deep', }; } - const template = cliOptions.gitStrategy - ? 'Git repository' - : ( - (await prompts( + if (await fs.pathExists(path.resolve(reqTemplate))) { + return { + type: 'local', + path: path.resolve(reqTemplate), + }; + } + return getTemplateSource({ + templateName: reqTemplate, + templates, + cliOptions, + }); +} + +async function askGitRepositorySource({ + cliOptions, +}: { + cliOptions: CLIOptions; +}): Promise { + const {gitRepoUrl} = (await prompts( + { + type: 'text', + name: 'gitRepoUrl', + validate: (url?: string) => { + if (url && isValidGitRepoUrl(url)) { + return true; + } + return logger.red('Invalid repository URL'); + }, + message: logger.interpolate`Enter a repository URL from GitHub, Bitbucket, GitLab, or any other public repo. +(e.g: url=${'https://github.com/ownerName/repoName.git'})`, + }, + { + onCancel() { + logger.error('A git repo URL is required.'); + process.exit(1); + }, + }, + )) as {gitRepoUrl: string}; + let strategy = cliOptions.gitStrategy; + if (!strategy) { + ({strategy} = (await prompts( + { + type: 'select', + name: 'strategy', + message: 'How should we clone this repo?', + choices: [ + {title: 'Deep clone: preserve full history', value: 'deep'}, + {title: 'Shallow clone: clone with --depth=1', value: 'shallow'}, { - type: 'select', - name: 'template', - message: 'Select a template below...', - choices: createTemplateChoices(templates), + title: 'Copy: do a shallow clone, but do not create a git repo', + value: 'copy', }, { - onCancel() { - logger.error('A choice is required.'); - process.exit(1); - }, + title: 'Custom: enter your custom git clone command', + value: 'custom', }, - )) as {template: Template | 'Git repository' | 'Local template'} - ).template; - if (template === 'Git repository') { - const {gitRepoUrl} = (await prompts( - { - type: 'text', - name: 'gitRepoUrl', - validate: (url?: string) => { - if (url && isValidGitRepoUrl(url)) { - return true; - } - return logger.red('Invalid repository URL'); - }, - message: logger.interpolate`Enter a repository URL from GitHub, Bitbucket, GitLab, or any other public repo. -(e.g: url=${'https://github.com/ownerName/repoName.git'})`, + ], }, { onCancel() { - logger.error('A git repo URL is required.'); - process.exit(1); + logger.info`Falling back to name=${'deep'}`; }, }, - )) as {gitRepoUrl: string}; - let strategy = cliOptions.gitStrategy; - if (!strategy) { - ({strategy} = (await prompts( - { - type: 'select', - name: 'strategy', - message: 'How should we clone this repo?', - choices: [ - {title: 'Deep clone: preserve full history', value: 'deep'}, - {title: 'Shallow clone: clone with --depth=1', value: 'shallow'}, - { - title: 'Copy: do a shallow clone, but do not create a git repo', - value: 'copy', - }, - { - title: 'Custom: enter your custom git clone command', - value: 'custom', - }, - ], - }, - { - onCancel() { - logger.info`Falling back to name=${'deep'}`; - }, - }, - )) as {strategy?: GitStrategy}); - } - return { - type: 'git', - url: gitRepoUrl, - strategy: strategy ?? 'deep', - }; - } else if (template === 'Local template') { - const {templateDir} = (await prompts( - { - type: 'text', - name: 'templateDir', - validate: async (dir?: string) => { - if (dir) { - const fullDir = path.resolve(dir); - if (await fs.pathExists(fullDir)) { - return true; - } - return logger.red( - logger.interpolate`path=${fullDir} does not exist.`, - ); + )) as {strategy?: GitStrategy}); + } + return { + type: 'git', + url: gitRepoUrl, + strategy: strategy ?? 'deep', + }; +} + +async function askLocalSource(): Promise { + const {templateDir} = (await prompts( + { + type: 'text', + name: 'templateDir', + validate: async (dir?: string) => { + if (dir) { + const fullDir = path.resolve(dir); + if (await fs.pathExists(fullDir)) { + return true; } - return logger.red('Please enter a valid path.'); - }, - message: - 'Enter a local folder path, relative to the current working directory.', + return logger.red( + logger.interpolate`path=${fullDir} does not exist.`, + ); + } + return logger.red('Please enter a valid path.'); }, - { - onCancel() { - logger.error('A file path is required.'); - process.exit(1); - }, + message: + 'Enter a local folder path, relative to the current working directory.', + }, + { + onCancel() { + logger.error('A file path is required.'); + process.exit(1); }, - )) as {templateDir: string}; - return { - type: 'local', - path: templateDir, - }; + }, + )) as {templateDir: string}; + return { + type: 'local', + path: templateDir, + }; +} + +async function getSource( + reqTemplate: string | undefined, + templates: Template[], + cliOptions: CLIOptions, +): Promise { + if (reqTemplate) { + return getUserProvidedSource({reqTemplate, templates, cliOptions}); } - let useTS = language.typescript; - if (!useTS && template.tsVariantPath) { - ({useTS} = (await prompts({ - type: 'confirm', - name: 'useTS', - message: - 'This template is available in TypeScript. Do you want to use the TS variant?', - initial: false, - })) as {useTS?: boolean}); + const template = await askTemplateChoice({templates, cliOptions}); + if (template === 'Git repository') { + return askGitRepositorySource({cliOptions}); } - return { - type: 'template', + if (template === 'Local template') { + return askLocalSource(); + } + return createTemplateSource({ template, - typescript: useTS ?? false, - }; + cliOptions, + }); } async function updatePkg(pkgPath: string, obj: {[key: string]: unknown}) { @@ -468,14 +523,8 @@ export default async function init( getSiteName(reqName, rootDir), ]); const dest = path.resolve(rootDir, siteName); - const {typescript, javascript} = cliOptions; - const languageOptions = {typescript, javascript}; - const noTsVersionAvailable = !getTemplate(templates, reqTemplate, typescript) - .tsVariantPath; - - const language = await getLanguage(languageOptions, noTsVersionAvailable); - const source = await getSource(reqTemplate, templates, language, cliOptions); + const source = await getSource(reqTemplate, templates, cliOptions); logger.info('Creating new Docusaurus project...'); @@ -493,7 +542,7 @@ export default async function init( } } else if (source.type === 'template') { try { - await copyTemplate(source.template, dest, source.typescript); + await copyTemplate(source.template, dest, source.language); } catch (err) { logger.error`Copying Docusaurus template name=${source.template.name} failed!`; throw err; diff --git a/packages/docusaurus-utils/src/cliUtils.ts b/packages/docusaurus-utils/src/cliUtils.ts index 6bec0d99dc9a..53eaac7a72b5 100644 --- a/packages/docusaurus-utils/src/cliUtils.ts +++ b/packages/docusaurus-utils/src/cliUtils.ts @@ -5,40 +5,61 @@ * LICENSE file in the root directory of this source tree. */ -import prompts from 'prompts'; +import prompts, {type Choice} from 'prompts'; import logger from '@docusaurus/logger'; -export type LanguagesOptions = { - javascript?: boolean; - typescript?: boolean; +type PreferredLanguage = 'javascript' | 'typescript'; + +type AskPreferredLanguageOptions = { + fallback: PreferredLanguage | undefined; + exit: boolean; }; -export async function getLanguage( - languages: LanguagesOptions, - noTsVersionAvailable?: boolean, -): Promise { - if (languages.typescript || languages.javascript) { - return languages; - } - if (noTsVersionAvailable) { - return {javascript: true}; +const DefaultOptions: AskPreferredLanguageOptions = { + fallback: undefined, + exit: false, +}; + +const ExitChoice: Choice = {title: logger.yellow('[Exit]'), value: '[Exit]'}; + +export async function askPreferredLanguage( + options: Partial = {}, +): Promise<'javascript' | 'typescript'> { + const {fallback, exit} = {...DefaultOptions, ...options}; + + const choices: Choice[] = [ + {title: logger.bold('JavaScript'), value: 'javascript'}, + {title: logger.bold('TypeScript'), value: 'typescript'}, + ]; + if (exit) { + choices.push(ExitChoice); } - const {language: selectedLanguage} = (await prompts( + + const {language} = await prompts( { type: 'select', name: 'language', message: 'Which language do you want to use?', - choices: [ - {title: 'JavaScript', value: 'javascript'}, - {title: 'TypeScript', value: 'typescript'}, - ], + choices, }, { onCancel() { - logger.info`Falling back to language=${'javascript'}`; + exit && process.exit(0); }, }, - )) as {language: keyof LanguagesOptions}; + ); + + if (language === ExitChoice.value) { + process.exit(0); + } + + if (!language) { + if (fallback) { + logger.info`Falling back to language=${fallback}`; + return fallback; + } + process.exit(0); + } - return {[selectedLanguage]: true}; + return language; } diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 06a9d60c0544..dc5fc1e1bf39 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -117,4 +117,4 @@ export { } from './dataFileUtils'; export {isDraft, isUnlisted} from './contentVisibilityUtils'; export {escapeRegexp} from './regExpUtils'; -export {getLanguage, type LanguagesOptions} from './cliUtils'; +export {askPreferredLanguage} from './cliUtils';