Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,5 +129,6 @@
"stylelint-config-standard": "^29.0.0",
"typescript": "~5.8.2"
},
"resolutions": {},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
9 changes: 5 additions & 4 deletions packages/create-docusaurus/bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@

// @ts-check

import path from 'path';
import {createRequire} from 'module';
import path from 'node:path';
import {inspect} from 'node:util';
import {createRequire} from 'node:module';
import {logger} from '@docusaurus/logger';
import semver from 'semver';
import {program} from 'commander';
Expand Down Expand Up @@ -61,7 +62,7 @@ if (!process.argv.slice(1).length) {
program.outputHelp();
}

process.on('unhandledRejection', (err) => {
logger.error(err);
process.on('unhandledRejection', (error) => {
logger.error(inspect(error));
process.exit(1);
});
4 changes: 1 addition & 3 deletions packages/create-docusaurus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,8 @@
"license": "MIT",
"dependencies": {
"@docusaurus/logger": "3.9.2",
"@docusaurus/utils": "3.9.2",
"commander": "^5.1.0",
"execa": "^5.1.1",
"fs-extra": "^11.1.1",
"cross-spawn": "^7.0.0",
"prompts": "^2.4.2",
"semver": "^7.5.4",
"supports-color": "^9.4.0",
Expand Down
120 changes: 71 additions & 49 deletions packages/create-docusaurus/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@
* LICENSE file in the root directory of this source tree.
*/

import fs from 'fs-extra';
import {fileURLToPath} from 'url';
import path from 'path';
import * as fs from 'node:fs/promises';
import {fileURLToPath} from 'node:url';
import path from 'node:path';

// KEEP DEPENDENCY SMALL HERE!
// create-docusaurus CLI should be as lightweight as possible

// TODO try to remove these third-party dependencies if possible
import {logger} from '@docusaurus/logger';
import execa from 'execa';
import prompts, {type Choice} from 'prompts';
import supportsColor from 'supports-color';

// TODO remove dependency on large @docusaurus/utils
// would be better to have a new smaller @docusaurus/utils-cli package
import {askPreferredLanguage} from '@docusaurus/utils';
import {siteNameToPackageName} from './utils.js';
import {runCommand, siteNameToPackageName} from './utils.js';
import {askPreferredLanguage} from './prompts.js';

type LanguagesOptions = {
javascript?: boolean;
Expand Down Expand Up @@ -54,12 +56,18 @@ type PackageManager = keyof typeof lockfileNames;

const packageManagers = Object.keys(lockfileNames) as PackageManager[];

function pathExists(filePath: string): Promise<boolean> {
return fs
.access(filePath, fs.constants.F_OK)
.then(() => true)
.catch(() => false);
}
async function findPackageManagerFromLockFile(
rootDir: string,
): Promise<PackageManager | undefined> {
for (const packageManager of packageManagers) {
const lockFilePath = path.join(rootDir, lockfileNames[packageManager]);
if (await fs.pathExists(lockFilePath)) {
if (await pathExists(lockFilePath)) {
return packageManager;
}
}
Expand All @@ -73,9 +81,9 @@ function findPackageManagerFromUserAgent(): PackageManager | undefined {
}

async function askForPackageManagerChoice(): Promise<PackageManager> {
const hasYarn = (await execa.command('yarn --version')).exitCode === 0;
const hasPnpm = (await execa.command('pnpm --version')).exitCode === 0;
const hasBun = (await execa.command('bun --version')).exitCode === 0;
const hasYarn = (await runCommand('yarn --version')) === 0;
const hasPnpm = (await runCommand('pnpm --version')) === 0;
const hasBun = (await runCommand('bun --version')) === 0;

if (!hasYarn && !hasPnpm && !hasBun) {
return 'npm';
Expand Down Expand Up @@ -156,7 +164,7 @@ async function readTemplates(): Promise<Template[]> {
return {
name,
path: path.join(templatesDir, name),
tsVariantPath: (await fs.pathExists(tsVariantPath))
tsVariantPath: (await pathExists(tsVariantPath))
? tsVariantPath
: undefined,
};
Expand All @@ -180,12 +188,15 @@ async function copyTemplate(
dest: string,
language: 'javascript' | 'typescript',
): Promise<void> {
await fs.copy(path.join(templatesDir, 'shared'), dest);
await fs.cp(path.join(templatesDir, 'shared'), dest, {
recursive: true,
});

const sourcePath =
language === 'typescript' ? template.tsVariantPath! : template.path;

await fs.copy(sourcePath, dest, {
await fs.cp(sourcePath, dest, {
recursive: true,
// 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(),
Expand Down Expand Up @@ -284,7 +295,7 @@ async function getSiteName(
if (siteName === '.' && (await fs.readdir(dest)).length > 0) {
return logger.interpolate`Directory not empty at path=${dest}!`;
}
if (siteName !== '.' && (await fs.pathExists(dest))) {
if (siteName !== '.' && (await pathExists(dest))) {
return logger.interpolate`Directory already exists at path=${dest}!`;
}
return true;
Expand Down Expand Up @@ -392,7 +403,7 @@ async function getUserProvidedSource({
strategy: cliOptions.gitStrategy ?? 'deep',
};
}
if (await fs.pathExists(path.resolve(reqTemplate))) {
if (await pathExists(path.resolve(reqTemplate))) {
return {
type: 'local',
path: path.resolve(reqTemplate),
Expand Down Expand Up @@ -472,7 +483,7 @@ async function askLocalSource(): Promise<Source> {
validate: async (dir?: string) => {
if (dir) {
const fullDir = path.resolve(dir);
if (await fs.pathExists(fullDir)) {
if (await pathExists(fullDir)) {
return true;
}
return logger.red(
Expand Down Expand Up @@ -520,10 +531,13 @@ async function getSource(
}

async function updatePkg(pkgPath: string, obj: {[key: string]: unknown}) {
const pkg = (await fs.readJSON(pkgPath)) as {[key: string]: unknown};
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8')) as {
[key: string]: unknown;
};
const newPkg = Object.assign(pkg, obj);

await fs.outputFile(pkgPath, `${JSON.stringify(newPkg, null, 2)}\n`);
await fs.mkdir(path.dirname(pkgPath), {recursive: true});
await fs.writeFile(pkgPath, `${JSON.stringify(newPkg, null, 2)}\n`);
}

export default async function init(
Expand All @@ -544,26 +558,33 @@ export default async function init(

if (source.type === 'git') {
const gitCommand = await getGitCommand(source.strategy);
if ((await execa(gitCommand, [source.url, dest])).exitCode !== 0) {
if ((await runCommand(gitCommand, [source.url, dest])) !== 0) {
logger.error`Cloning Git template failed!`;
process.exit(1);
}
if (source.strategy === 'copy') {
await fs.remove(path.join(dest, '.git'));
await fs.rm(path.join(dest, '.git'), {
force: true,
recursive: true,
});
}
} else if (source.type === 'template') {
try {
await copyTemplate(source.template, dest, source.language);
} catch (err) {
logger.error`Copying Docusaurus template name=${source.template.name} failed!`;
throw err;
throw new Error(
logger.interpolate`Copying Docusaurus template name=${source.template.name} failed!`,
{cause: err},
);
}
} else {
try {
await fs.copy(source.path, dest);
await fs.cp(source.path, dest, {recursive: true});
} catch (err) {
logger.error`Copying local template path=${source.path} failed!`;
throw err;
throw new Error(
logger.interpolate`Copying local template path=${source.path} failed!`,
{cause: err},
);
}
}

Expand All @@ -575,19 +596,21 @@ export default async function init(
private: true,
});
} catch (err) {
logger.error('Failed to update package.json.');
throw err;
throw new Error('Failed to update package.json.', {cause: err});
}

// We need to rename the gitignore file to .gitignore
if (
!(await fs.pathExists(path.join(dest, '.gitignore'))) &&
(await fs.pathExists(path.join(dest, 'gitignore')))
!(await pathExists(path.join(dest, '.gitignore'))) &&
(await pathExists(path.join(dest, 'gitignore')))
) {
await fs.move(path.join(dest, 'gitignore'), path.join(dest, '.gitignore'));
await fs.rename(
path.join(dest, 'gitignore'),
path.join(dest, '.gitignore'),
);
}
if (await fs.pathExists(path.join(dest, 'gitignore'))) {
await fs.remove(path.join(dest, 'gitignore'));
if (await pathExists(path.join(dest, 'gitignore'))) {
await fs.rm(path.join(dest, 'gitignore'));
}

// Display the most elegant way to cd.
Expand All @@ -599,22 +622,21 @@ export default async function init(
// ...

if (
(
await execa.command(
pkgManager === 'yarn'
? 'yarn'
: pkgManager === 'bun'
? 'bun install'
: `${pkgManager} install --color always`,
{
env: {
...process.env,
// Force coloring the output
...(supportsColor.stdout ? {FORCE_COLOR: '1'} : {}),
},
(await runCommand(
pkgManager === 'yarn'
? 'yarn'
: pkgManager === 'bun'
? 'bun install'
: `${pkgManager} install --color always`,
[],
{
env: {
...process.env,
// Force coloring the output
...(supportsColor.stdout ? {FORCE_COLOR: '1'} : {}),
},
)
).exitCode !== 0
},
)) !== 0
) {
logger.error('Dependency installation failed.');
logger.info`The site directory has already been created, and you can retry by typing:
Expand Down
27 changes: 27 additions & 0 deletions packages/create-docusaurus/src/prompts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import prompts from 'prompts';
import {logger} from '@docusaurus/logger';

export async function askPreferredLanguage(): Promise<
'javascript' | 'typescript'
> {
const {language} = await prompts({
type: 'select',
name: 'language',
message: 'Which language do you want to use?',
choices: [
{title: logger.bold('JavaScript'), value: 'javascript'},
{title: logger.bold('TypeScript'), value: 'typescript'},
],
});
if (!language) {
process.exit(0);
}
return language;
}
8 changes: 8 additions & 0 deletions packages/create-docusaurus/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

export type PackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun';
41 changes: 41 additions & 0 deletions packages/create-docusaurus/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,47 @@
* LICENSE file in the root directory of this source tree.
*/

// @ts-expect-error: no types, but same as spawn()
import CrossSpawn from 'cross-spawn';
import type {spawn, SpawnOptions} from 'node:child_process';

// We use cross-spawn instead of spawn because of Windows compatibility issues.
// For example, "yarn" doesn't work on Windows, it requires "yarn.cmd"
// Tools like execa() use cross-spawn under the hood, and "resolve" the command
const crossSpawn: typeof spawn = CrossSpawn;

/**
* Run a command, similar to execa(cmd,args) but simpler
* @param command
* @param args
* @param options
* @returns the command exit code
*/
export async function runCommand(
command: string,
args: string[] = [],
options: SpawnOptions = {},
): Promise<number> {
// This does something similar to execa.command()
// we split a string command (with optional args) into command+args
// this way it's compatible with spawn()
const [realCommand, ...baseArgs] = command.split(' ');
const allArgs = [...baseArgs, ...args];
if (!realCommand) {
throw new Error(`Invalid command: ${command}`);
}

return new Promise<number>((resolve, reject) => {
const p = crossSpawn(realCommand, allArgs, {stdio: 'ignore', ...options});
p.on('error', reject);
p.on('close', (exitCode) =>
exitCode !== null
? resolve(exitCode)
: reject(new Error(`No exit code for command ${command}`)),
);
});
}

/**
* We use a simple kebab-case-like conversion
* It's not perfect, but good enough
Expand Down
Loading
Loading