mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-06 12:52:31 +02:00
feat(create-docusaurus): ask user for preferred language when no language CLI option provided (#9442)
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
This commit is contained in:
parent
6fd8408a2c
commit
628752d92a
8 changed files with 301 additions and 160 deletions
|
@ -13,15 +13,29 @@ import logger from '@docusaurus/logger';
|
|||
import shell from 'shelljs';
|
||||
import prompts, {type Choice} from 'prompts';
|
||||
import supportsColor from 'supports-color';
|
||||
import {escapeShellArg} from '@docusaurus/utils';
|
||||
import {escapeShellArg, askPreferredLanguage} from '@docusaurus/utils';
|
||||
|
||||
type CLIOptions = {
|
||||
type LanguagesOptions = {
|
||||
javascript?: boolean;
|
||||
typescript?: boolean;
|
||||
};
|
||||
|
||||
type CLIOptions = LanguagesOptions & {
|
||||
packageManager?: PackageManager;
|
||||
skipInstall?: boolean;
|
||||
typescript?: boolean;
|
||||
gitStrategy?: GitStrategy;
|
||||
};
|
||||
|
||||
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
|
||||
const defaultPackageManager = 'npm';
|
||||
|
@ -153,11 +167,14 @@ async function readTemplates(): Promise<Template[]> {
|
|||
async function copyTemplate(
|
||||
template: Template,
|
||||
dest: string,
|
||||
typescript: boolean,
|
||||
language: 'javascript' | 'typescript',
|
||||
): Promise<void> {
|
||||
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(),
|
||||
|
@ -183,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));
|
||||
}
|
||||
|
@ -260,7 +304,7 @@ type Source =
|
|||
| {
|
||||
type: 'template';
|
||||
template: Template;
|
||||
typescript: boolean;
|
||||
language: 'javascript' | 'typescript';
|
||||
}
|
||||
| {
|
||||
type: 'git';
|
||||
|
@ -272,166 +316,193 @@ type Source =
|
|||
path: string;
|
||||
};
|
||||
|
||||
async function createTemplateSource({
|
||||
template,
|
||||
cliOptions,
|
||||
}: {
|
||||
template: Template;
|
||||
cliOptions: CLIOptions;
|
||||
}): Promise<Source> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
async function getTemplateSource({
|
||||
templateName,
|
||||
templates,
|
||||
cliOptions,
|
||||
}: {
|
||||
templateName: string;
|
||||
templates: Template[];
|
||||
cliOptions: CLIOptions;
|
||||
}): Promise<Source> {
|
||||
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<Source> {
|
||||
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',
|
||||
};
|
||||
}
|
||||
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<Source> {
|
||||
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'},
|
||||
{
|
||||
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',
|
||||
};
|
||||
}
|
||||
|
||||
async function askLocalSource(): Promise<Source> {
|
||||
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.`,
|
||||
);
|
||||
}
|
||||
return logger.red('Please enter a valid path.');
|
||||
},
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
async function getSource(
|
||||
reqTemplate: string | undefined,
|
||||
templates: Template[],
|
||||
cliOptions: CLIOptions,
|
||||
): Promise<Source> {
|
||||
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),
|
||||
};
|
||||
}
|
||||
const template = templates.find((t) => t.name === reqTemplate);
|
||||
if (!template) {
|
||||
logger.error('Invalid template.');
|
||||
process.exit(1);
|
||||
}
|
||||
if (cliOptions.typescript && !template.tsVariantPath) {
|
||||
logger.error`Template name=${reqTemplate} doesn't provide the TypeScript variant.`;
|
||||
process.exit(1);
|
||||
}
|
||||
return {
|
||||
type: 'template',
|
||||
template,
|
||||
typescript: cliOptions.typescript ?? false,
|
||||
};
|
||||
return getUserProvidedSource({reqTemplate, templates, cliOptions});
|
||||
}
|
||||
const template = 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;
|
||||
|
||||
const template = await askTemplateChoice({templates, cliOptions});
|
||||
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);
|
||||
},
|
||||
},
|
||||
)) 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.`,
|
||||
);
|
||||
}
|
||||
return logger.red('Please enter a valid path.');
|
||||
},
|
||||
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,
|
||||
};
|
||||
return askGitRepositorySource({cliOptions});
|
||||
}
|
||||
let useTS = cliOptions.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});
|
||||
if (template === 'Local template') {
|
||||
return askLocalSource();
|
||||
}
|
||||
return {
|
||||
type: 'template',
|
||||
return createTemplateSource({
|
||||
template,
|
||||
typescript: useTS ?? false,
|
||||
};
|
||||
cliOptions,
|
||||
});
|
||||
}
|
||||
|
||||
async function updatePkg(pkgPath: string, obj: {[key: string]: unknown}) {
|
||||
|
@ -452,6 +523,7 @@ export default async function init(
|
|||
getSiteName(reqName, rootDir),
|
||||
]);
|
||||
const dest = path.resolve(rootDir, siteName);
|
||||
|
||||
const source = await getSource(reqTemplate, templates, cliOptions);
|
||||
|
||||
logger.info('Creating new Docusaurus project...');
|
||||
|
@ -470,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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue