mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 10:55:57 +00:00
CLI: Support CI environment variable, remove negatives options from CLI usage
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'create-fumadocs-app': patch
|
||||
---
|
||||
|
||||
Support `CI` environment variable, remove negatives options from CLI usage
|
||||
@@ -3,23 +3,22 @@
|
||||
"version": "16.0.2",
|
||||
"description": "Create a new documentation site with Fumadocs",
|
||||
"keywords": [
|
||||
"NextJs",
|
||||
"next",
|
||||
"react",
|
||||
"Docs"
|
||||
"Docs",
|
||||
"Fumadocs"
|
||||
],
|
||||
"homepage": "https://fumadocs.dev",
|
||||
"repository": "github:fuma-nama/fumadocs",
|
||||
"license": "MIT",
|
||||
"author": "Fuma Nama",
|
||||
"type": "module",
|
||||
"bin": "./dist/index.js",
|
||||
"module": "./dist/create-app.js",
|
||||
"types": "./dist/create-app.d.ts",
|
||||
"bin": "./dist/bin.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/create-app.js",
|
||||
"types": "./dist/create-app.d.ts"
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./plugins/*": {
|
||||
"import": "./dist/plugins/*.js",
|
||||
@@ -47,6 +46,7 @@
|
||||
"ts-morph": "^27.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commander-js/extra-typings": "^14.0.0",
|
||||
"@types/cross-spawn": "^6.0.6",
|
||||
"@types/node": "24.9.1",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
cancel,
|
||||
confirm,
|
||||
group,
|
||||
intro,
|
||||
isCancel,
|
||||
outro,
|
||||
select,
|
||||
spinner,
|
||||
text,
|
||||
} from '@clack/prompts';
|
||||
import pc from 'picocolors';
|
||||
import { getPackageManager, managers } from './auto-install';
|
||||
import { create, type Template, type TemplatePlugin } from './index';
|
||||
import { isCI, templates } from './constants';
|
||||
import { Option, program } from '@commander-js/extra-typings';
|
||||
|
||||
const command = program
|
||||
.argument('[name]', 'the project name')
|
||||
.option('--src', '(Next.js only) enable `src/` directory')
|
||||
.option('--install', 'install packages automatically')
|
||||
.option('--no-git', 'disable auto Git repository initialization')
|
||||
.addOption(
|
||||
new Option(
|
||||
'--linter <name>',
|
||||
'(Next.js only) configure a linter/formatter',
|
||||
).choices(['eslint', 'biome']),
|
||||
)
|
||||
.addOption(
|
||||
new Option('--search <name>', 'configure a search solution').choices([
|
||||
'orama',
|
||||
'orama-cloud',
|
||||
]),
|
||||
)
|
||||
.addOption(
|
||||
new Option('--template <name>', 'choose a template').choices(
|
||||
templates.map((item) => item.value),
|
||||
),
|
||||
)
|
||||
.addOption(
|
||||
new Option('--pm <name>', 'choose a package manager')
|
||||
.choices(managers)
|
||||
.default(getPackageManager()),
|
||||
);
|
||||
|
||||
async function main(): Promise<void> {
|
||||
command.parse(process.argv);
|
||||
const defaultName = command.args[0];
|
||||
const config = command.opts();
|
||||
intro(pc.bgCyan(pc.bold('Create Fumadocs App')));
|
||||
|
||||
const options = await group(
|
||||
{
|
||||
name: async () => {
|
||||
if (defaultName) return defaultName;
|
||||
if (isCI) return 'untitled';
|
||||
|
||||
return text({
|
||||
message: 'Project name',
|
||||
placeholder: 'my-app',
|
||||
defaultValue: 'my-app',
|
||||
});
|
||||
},
|
||||
template: async () => {
|
||||
if (config.template) return config.template;
|
||||
if (isCI) return '+next+fuma-docs-mdx';
|
||||
|
||||
return select<Template>({
|
||||
message: 'Choose a template',
|
||||
initialValue: '+next+fuma-docs-mdx',
|
||||
options: templates,
|
||||
});
|
||||
},
|
||||
src: async ({ results }: { results: { template?: Template } }) => {
|
||||
if (config.src !== undefined) return config.src;
|
||||
if (isCI || !results.template?.startsWith('+next')) return false;
|
||||
|
||||
return confirm({
|
||||
message: 'Use `/src` directory?',
|
||||
initialValue: false,
|
||||
});
|
||||
},
|
||||
lint: async ({ results }: { results: { template?: Template } }) => {
|
||||
if (config.linter !== undefined) return config.linter;
|
||||
if (isCI || !results.template?.startsWith('+next')) return 'disabled';
|
||||
|
||||
return select({
|
||||
message: 'Configure linter?',
|
||||
options: [
|
||||
{
|
||||
value: 'disabled',
|
||||
label: 'Disabled',
|
||||
},
|
||||
{
|
||||
value: 'eslint',
|
||||
label: 'ESLint',
|
||||
},
|
||||
{
|
||||
value: 'biome',
|
||||
label: 'Biome',
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
search: async () => {
|
||||
if (config.search !== undefined) return config.search;
|
||||
if (isCI) return 'orama';
|
||||
|
||||
return select({
|
||||
message: 'Choose a search solution?',
|
||||
options: [
|
||||
{
|
||||
value: 'orama',
|
||||
label: 'Default',
|
||||
hint: 'local search powered by Orama, recommended',
|
||||
},
|
||||
{
|
||||
value: 'orama-cloud',
|
||||
label: 'Orama Cloud',
|
||||
hint: '3rd party search solution, signup needed',
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
installDeps: async () => {
|
||||
if (config.install !== undefined) return config.install;
|
||||
if (isCI) return false;
|
||||
|
||||
return confirm({
|
||||
message: `Do you want to install packages automatically? (detected as ${config.pm})`,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
cancel('Installation Stopped.');
|
||||
process.exit(0);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const projectName = options.name.toLowerCase().replace(/\s/, '-');
|
||||
if (!isCI) await checkDir(projectName);
|
||||
|
||||
const info = spinner();
|
||||
info.start(`Generating Project`);
|
||||
const plugins: TemplatePlugin[] = [];
|
||||
|
||||
if (options.src) {
|
||||
const { nextUseSrc } = await import('./plugins/next-use-src');
|
||||
plugins.push(nextUseSrc());
|
||||
}
|
||||
|
||||
if (options.search === 'orama-cloud') {
|
||||
const { oramaCloud } = await import('./plugins/orama-cloud');
|
||||
plugins.push(oramaCloud());
|
||||
}
|
||||
|
||||
if (options.lint === 'eslint') {
|
||||
const { eslint } = await import('./plugins/eslint');
|
||||
plugins.push(eslint());
|
||||
}
|
||||
|
||||
if (options.lint === 'biome') {
|
||||
const { biome } = await import('./plugins/biome');
|
||||
plugins.push(biome());
|
||||
}
|
||||
|
||||
await create({
|
||||
packageManager: config.pm,
|
||||
template: options.template,
|
||||
outputDir: projectName,
|
||||
installDeps: options.installDeps,
|
||||
initializeGit: config.git,
|
||||
plugins,
|
||||
log: (message) => {
|
||||
info.message(message);
|
||||
},
|
||||
});
|
||||
|
||||
info.stop('Project Generated');
|
||||
|
||||
outro(pc.bgGreen(pc.bold('Done')));
|
||||
|
||||
console.log(pc.bold('\nOpen the project'));
|
||||
console.log(pc.cyan(`cd ${projectName}`));
|
||||
|
||||
console.log(pc.bold('\nRun Development Server'));
|
||||
if (config.pm === 'npm' || config.pm === 'bun') {
|
||||
console.log(pc.cyan(`${config.pm} run dev`));
|
||||
} else {
|
||||
console.log(pc.cyan(`${config.pm} dev`));
|
||||
}
|
||||
console.log(
|
||||
pc.bold('\nYou can now open the project and start writing documents'),
|
||||
);
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async function checkDir(outputDir: string) {
|
||||
const destDir = await fs.readdir(outputDir).catch(() => null);
|
||||
if (!destDir || destDir.length === 0) return;
|
||||
const del = await confirm({
|
||||
message: `directory ${outputDir} already exists, do you want to delete its files?`,
|
||||
});
|
||||
|
||||
if (isCancel(del)) {
|
||||
cancel();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!del) return;
|
||||
|
||||
const info = spinner();
|
||||
info.start(`Deleting files in ${outputDir}`);
|
||||
|
||||
await Promise.all(
|
||||
destDir.map((item) => {
|
||||
return fs.rm(path.join(outputDir, item), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
info.stop(`Deleted files in ${outputDir}`);
|
||||
}
|
||||
|
||||
main().catch((e: unknown) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -4,6 +4,8 @@ import versionPkg from '../../create-app-versions/package.json';
|
||||
|
||||
export const sourceDir = fileURLToPath(new URL(`../`, import.meta.url).href);
|
||||
|
||||
export const isCI = Boolean(process.env.CI);
|
||||
|
||||
export interface TemplateInfo {
|
||||
value:
|
||||
| '+next+fuma-docs-mdx'
|
||||
@@ -13,6 +15,9 @@ export interface TemplateInfo {
|
||||
| 'tanstack-start';
|
||||
label: string;
|
||||
appDir: string;
|
||||
/**
|
||||
* path to root provider, relative to `appDir``
|
||||
*/
|
||||
rootProviderPath: string;
|
||||
hint?: string;
|
||||
/**
|
||||
@@ -33,26 +38,26 @@ export const templates: TemplateInfo[] = [
|
||||
value: 'waku',
|
||||
label: 'Waku: Fumadocs MDX',
|
||||
appDir: 'src',
|
||||
rootProviderPath: 'src/components/provider.tsx',
|
||||
rootProviderPath: 'components/provider.tsx',
|
||||
},
|
||||
{
|
||||
value: 'react-router',
|
||||
label: 'React Router: Fumadocs MDX (not RSC)',
|
||||
appDir: 'app',
|
||||
rootProviderPath: 'app/root.tsx',
|
||||
rootProviderPath: 'root.tsx',
|
||||
},
|
||||
{
|
||||
value: 'react-router-spa',
|
||||
label: 'React Router SPA: Fumadocs MDX (not RSC)',
|
||||
hint: 'SPA mode allows you to host the site statically, compatible with a CDN.',
|
||||
appDir: 'app',
|
||||
rootProviderPath: 'app/root.tsx',
|
||||
rootProviderPath: 'root.tsx',
|
||||
},
|
||||
{
|
||||
value: 'tanstack-start',
|
||||
label: 'Tanstack Start: Fumadocs MDX (not RSC)',
|
||||
appDir: 'src',
|
||||
rootProviderPath: 'src/routes/__root.tsx',
|
||||
rootProviderPath: 'routes/__root.tsx',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import { copy, tryGitInit } from '@/utils';
|
||||
import type { PackageManager } from './auto-install';
|
||||
import { autoInstall } from './auto-install';
|
||||
import {
|
||||
depVersions,
|
||||
sourceDir,
|
||||
type TemplateInfo,
|
||||
templates,
|
||||
} from './constants';
|
||||
|
||||
export type Template = TemplateInfo['value'];
|
||||
export interface Options {
|
||||
outputDir: string;
|
||||
template: Template;
|
||||
|
||||
/**
|
||||
* the package manager to use
|
||||
*
|
||||
* @defaultValue 'npm'
|
||||
*/
|
||||
packageManager?: PackageManager;
|
||||
|
||||
installDeps?: boolean;
|
||||
initializeGit?: boolean;
|
||||
log?: (message: string) => void;
|
||||
plugins?: TemplatePlugin[];
|
||||
}
|
||||
|
||||
export interface TemplatePluginContext {
|
||||
template: TemplateInfo;
|
||||
log: (message: string) => void;
|
||||
/**
|
||||
* output directory
|
||||
*/
|
||||
dest: string;
|
||||
|
||||
/**
|
||||
* output directory for app code (e.g. under `/src`)
|
||||
*/
|
||||
appDir: string;
|
||||
}
|
||||
|
||||
export type PackageJsonType = {
|
||||
name?: string;
|
||||
version?: string;
|
||||
private?: boolean;
|
||||
scripts?: Record<string, string>;
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
} & Record<string, unknown>;
|
||||
|
||||
type Awaitable<T> = T | Promise<T>;
|
||||
|
||||
export interface TemplatePlugin {
|
||||
template?: (
|
||||
this: Pick<TemplatePluginContext, 'dest'>,
|
||||
info: TemplateInfo,
|
||||
) => Awaitable<void | TemplateInfo>;
|
||||
packageJson?: (
|
||||
this: TemplatePluginContext,
|
||||
packageJson: PackageJsonType,
|
||||
) => Awaitable<void | PackageJsonType>;
|
||||
afterWrite?: (this: TemplatePluginContext) => Awaitable<void>;
|
||||
readme?: (
|
||||
this: TemplatePluginContext,
|
||||
content: string,
|
||||
) => Awaitable<void | string>;
|
||||
}
|
||||
|
||||
export async function create(createOptions: Options): Promise<void> {
|
||||
const {
|
||||
outputDir,
|
||||
plugins = [],
|
||||
packageManager = 'npm',
|
||||
initializeGit = false,
|
||||
installDeps = false,
|
||||
log = console.log,
|
||||
} = createOptions;
|
||||
|
||||
let template = templates.find(
|
||||
(item) => item.value === createOptions.template,
|
||||
)!;
|
||||
for (const plugin of plugins) {
|
||||
template =
|
||||
(await plugin.template?.call({ dest: outputDir }, template)) ?? template;
|
||||
}
|
||||
|
||||
const appDir = path.join(outputDir, template.appDir);
|
||||
const projectName = path.basename(outputDir);
|
||||
const pluginContext: TemplatePluginContext = {
|
||||
template,
|
||||
dest: outputDir,
|
||||
log,
|
||||
appDir,
|
||||
};
|
||||
|
||||
await copy(path.join(sourceDir, 'template', template.value), outputDir, {
|
||||
rename(file) {
|
||||
file = file.replace('example.gitignore', '.gitignore');
|
||||
|
||||
return template.rename?.(file) ?? file;
|
||||
},
|
||||
});
|
||||
|
||||
const packageJsonPath = path.join(outputDir, 'package.json');
|
||||
let packageJson = await initPackageJson(projectName, packageJsonPath);
|
||||
for (const plugin of plugins) {
|
||||
packageJson =
|
||||
(await plugin.packageJson?.call(pluginContext, packageJson)) ??
|
||||
packageJson;
|
||||
}
|
||||
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
||||
|
||||
const readmePath = path.join(outputDir, 'README.md');
|
||||
let readme = `# ${projectName}\n\n${await fs.readFile(readmePath)}`;
|
||||
for (const plugin of plugins) {
|
||||
readme = (await plugin.readme?.call(pluginContext, readme)) ?? readme;
|
||||
}
|
||||
await fs.writeFile(readmePath, readme);
|
||||
|
||||
for (const plugin of plugins) {
|
||||
await plugin.afterWrite?.call(pluginContext);
|
||||
}
|
||||
|
||||
if (installDeps) {
|
||||
try {
|
||||
await autoInstall(packageManager, outputDir);
|
||||
log('Installed dependencies');
|
||||
} catch (err) {
|
||||
log(`Failed to install dependencies: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (initializeGit && (await tryGitInit(outputDir))) {
|
||||
log('Initialized Git repository');
|
||||
}
|
||||
}
|
||||
|
||||
async function initPackageJson(
|
||||
projectName: string,
|
||||
packageJsonPath: string,
|
||||
): Promise<PackageJsonType> {
|
||||
function replaceWorkspaceDeps(deps: Record<string, string> = {}) {
|
||||
for (const k in deps) {
|
||||
if (deps[k].startsWith('workspace:') && k in depVersions) {
|
||||
deps[k] = depVersions[k as keyof typeof depVersions];
|
||||
}
|
||||
}
|
||||
|
||||
return deps;
|
||||
}
|
||||
|
||||
const packageJson: PackageJsonType = JSON.parse(
|
||||
(await fs.readFile(packageJsonPath)).toString(),
|
||||
);
|
||||
|
||||
return {
|
||||
...packageJson,
|
||||
name: projectName,
|
||||
scripts: {
|
||||
...packageJson.scripts,
|
||||
postinstall: 'fumadocs-mdx',
|
||||
},
|
||||
dependencies: replaceWorkspaceDeps(packageJson.dependencies),
|
||||
devDependencies: replaceWorkspaceDeps(packageJson.devDependencies),
|
||||
};
|
||||
}
|
||||
+142
-237
@@ -1,264 +1,169 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import { copy, tryGitInit } from '@/utils';
|
||||
import type { PackageManager } from './auto-install';
|
||||
import { autoInstall } from './auto-install';
|
||||
import {
|
||||
cancel,
|
||||
confirm,
|
||||
group,
|
||||
intro,
|
||||
isCancel,
|
||||
outro,
|
||||
select,
|
||||
spinner,
|
||||
text,
|
||||
} from '@clack/prompts';
|
||||
import pc from 'picocolors';
|
||||
import {
|
||||
getPackageManager,
|
||||
managers,
|
||||
type PackageManager,
|
||||
} from './auto-install';
|
||||
import { create, type Template, TemplatePlugin } from './create-app';
|
||||
import { templates } from './constants';
|
||||
import { program } from 'commander';
|
||||
depVersions,
|
||||
sourceDir,
|
||||
type TemplateInfo,
|
||||
templates,
|
||||
} from './constants';
|
||||
|
||||
program.argument('[name]', 'the project name');
|
||||
program.option('--src', '(Next.js only) enable `src/` directory');
|
||||
program.option('--no-src', '(Next.js only) disable `src/` directory');
|
||||
export type Template = TemplateInfo['value'];
|
||||
export interface Options {
|
||||
outputDir: string;
|
||||
template: Template;
|
||||
|
||||
program.option('--eslint', '(Next.js only) enable ESLint configuration');
|
||||
program.option('--no-eslint', '(Next.js only) disable ESLint configuration');
|
||||
/**
|
||||
* the package manager to use
|
||||
*
|
||||
* @defaultValue 'npm'
|
||||
*/
|
||||
packageManager?: PackageManager;
|
||||
|
||||
program.option('--biome', '(Next.js only) enable Biome configuration');
|
||||
program.option('--no-biome', '(Next.js only) disable Biome configuration');
|
||||
|
||||
program.option('--install', 'Enable installing packages automatically');
|
||||
program.option('--no-install', 'Disable installing packages automatically');
|
||||
|
||||
program.option('--no-git', 'Disable auto Git repository initialization');
|
||||
|
||||
program.option(
|
||||
'--template <name>',
|
||||
`template to choose: ${templates.map((v) => v.value).join(', ')}`,
|
||||
(value) => {
|
||||
if (!templates.some((item) => item.value === value)) {
|
||||
throw new Error(`Invalid template: ${value}.`);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
);
|
||||
|
||||
program.option(
|
||||
'--pm <name>',
|
||||
`package manager to choose: ${managers.join(', ')}`,
|
||||
(value) => {
|
||||
if (!managers.includes(value as PackageManager)) {
|
||||
throw new Error(`Invalid package manager: ${value}.`);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
);
|
||||
|
||||
interface Options {
|
||||
name?: string;
|
||||
src?: boolean;
|
||||
eslint?: boolean;
|
||||
biome?: boolean;
|
||||
install?: boolean;
|
||||
template?: Template;
|
||||
pm?: PackageManager;
|
||||
git?: boolean;
|
||||
installDeps?: boolean;
|
||||
initializeGit?: boolean;
|
||||
log?: (message: string) => void;
|
||||
plugins?: TemplatePlugin[];
|
||||
}
|
||||
|
||||
async function main(config: Options): Promise<void> {
|
||||
intro(pc.bgCyan(pc.bold('Create Fumadocs App')));
|
||||
const manager = config.pm ?? getPackageManager();
|
||||
export interface TemplatePluginContext {
|
||||
template: TemplateInfo;
|
||||
log: (message: string) => void;
|
||||
/**
|
||||
* output directory
|
||||
*/
|
||||
dest: string;
|
||||
|
||||
type SrcOption = boolean;
|
||||
type LintOption = 'biome' | 'eslint' | false;
|
||||
const options = await group(
|
||||
{
|
||||
name: () => {
|
||||
if (config.name) return Promise.resolve(config.name);
|
||||
/**
|
||||
* output directory for app code (e.g. under `/src`)
|
||||
*/
|
||||
appDir: string;
|
||||
}
|
||||
|
||||
return text({
|
||||
message: 'Project name',
|
||||
placeholder: 'my-app',
|
||||
defaultValue: 'my-app',
|
||||
});
|
||||
},
|
||||
template: () => {
|
||||
if (config.template) return Promise.resolve(config.template);
|
||||
export type PackageJsonType = {
|
||||
name?: string;
|
||||
version?: string;
|
||||
private?: boolean;
|
||||
scripts?: Record<string, string>;
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
} & Record<string, unknown>;
|
||||
|
||||
return select<Template>({
|
||||
message: 'Choose a template',
|
||||
initialValue: '+next+fuma-docs-mdx',
|
||||
options: templates,
|
||||
});
|
||||
},
|
||||
src: async (v): Promise<SrcOption | symbol> => {
|
||||
if (!v.results.template?.startsWith('+next')) return false;
|
||||
if (config.src !== undefined) return config.src;
|
||||
type Awaitable<T> = T | Promise<T>;
|
||||
|
||||
return confirm({
|
||||
message: 'Use `/src` directory?',
|
||||
initialValue: false,
|
||||
});
|
||||
},
|
||||
lint: async (v): Promise<LintOption | symbol> => {
|
||||
if (!v.results.template?.startsWith('+next')) return false;
|
||||
export interface TemplatePlugin {
|
||||
template?: (
|
||||
this: Pick<TemplatePluginContext, 'dest'>,
|
||||
info: TemplateInfo,
|
||||
) => Awaitable<void | TemplateInfo>;
|
||||
packageJson?: (
|
||||
this: TemplatePluginContext,
|
||||
packageJson: PackageJsonType,
|
||||
) => Awaitable<void | PackageJsonType>;
|
||||
afterWrite?: (this: TemplatePluginContext) => Awaitable<void>;
|
||||
readme?: (
|
||||
this: TemplatePluginContext,
|
||||
content: string,
|
||||
) => Awaitable<void | string>;
|
||||
}
|
||||
|
||||
if (config.eslint !== undefined) {
|
||||
return config.eslint ? 'eslint' : false;
|
||||
}
|
||||
export async function create(createOptions: Options): Promise<void> {
|
||||
const {
|
||||
outputDir,
|
||||
plugins = [],
|
||||
packageManager = 'npm',
|
||||
initializeGit = false,
|
||||
installDeps = false,
|
||||
log = console.log,
|
||||
} = createOptions;
|
||||
|
||||
if (config.biome !== undefined) {
|
||||
return config.biome ? 'biome' : false;
|
||||
}
|
||||
|
||||
return select<LintOption>({
|
||||
message: 'Configure linter?',
|
||||
initialValue: false,
|
||||
options: [
|
||||
{
|
||||
value: false,
|
||||
label: 'Disabled',
|
||||
},
|
||||
{
|
||||
value: 'eslint',
|
||||
label: 'ESLint',
|
||||
},
|
||||
{
|
||||
value: 'biome',
|
||||
label: 'Biome',
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
search: () => {
|
||||
return select({
|
||||
message: 'Choose a search solution?',
|
||||
options: [
|
||||
{
|
||||
value: 'orama' as const,
|
||||
label: 'Default',
|
||||
hint: 'local search powered by Orama, recommended',
|
||||
},
|
||||
{
|
||||
value: 'orama-cloud' as const,
|
||||
label: 'Orama Cloud',
|
||||
hint: '3rd party search solution, signup needed',
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
installDeps: () => {
|
||||
if (config.install !== undefined)
|
||||
return Promise.resolve(config.install);
|
||||
|
||||
return confirm({
|
||||
message: `Do you want to install packages automatically? (detected as ${manager})`,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
cancel('Installation Stopped.');
|
||||
process.exit(0);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const projectName = options.name.toLowerCase().replace(/\s/, '-');
|
||||
const destDir = await fs.readdir(projectName).catch(() => null);
|
||||
if (destDir && destDir.length > 0) {
|
||||
const del = await confirm({
|
||||
message: `directory ${projectName} already exists, do you want to delete its files?`,
|
||||
});
|
||||
|
||||
if (isCancel(del)) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (del) {
|
||||
const info = spinner();
|
||||
info.start(`Deleting files in ${projectName}`);
|
||||
|
||||
await Promise.all(
|
||||
destDir.map((item) => {
|
||||
return fs.rm(path.join(projectName, item), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
info.stop(`Deleted files in ${projectName}`);
|
||||
}
|
||||
let template = templates.find(
|
||||
(item) => item.value === createOptions.template,
|
||||
)!;
|
||||
for (const plugin of plugins) {
|
||||
template =
|
||||
(await plugin.template?.call({ dest: outputDir }, template)) ?? template;
|
||||
}
|
||||
|
||||
const info = spinner();
|
||||
info.start(`Generating Project`);
|
||||
const plugins: TemplatePlugin[] = [];
|
||||
const appDir = path.join(outputDir, template.appDir);
|
||||
const projectName = path.basename(outputDir);
|
||||
const pluginContext: TemplatePluginContext = {
|
||||
template,
|
||||
dest: outputDir,
|
||||
log,
|
||||
appDir,
|
||||
};
|
||||
|
||||
if (options.src) {
|
||||
const { nextUseSrc } = await import('./plugins/next-use-src');
|
||||
plugins.push(nextUseSrc());
|
||||
}
|
||||
await copy(path.join(sourceDir, 'template', template.value), outputDir, {
|
||||
rename(file) {
|
||||
file = file.replace('example.gitignore', '.gitignore');
|
||||
|
||||
if (options.search === 'orama-cloud') {
|
||||
const { oramaCloud } = await import('./plugins/orama-cloud');
|
||||
plugins.push(oramaCloud());
|
||||
}
|
||||
|
||||
if (options.lint === 'eslint') {
|
||||
const { eslint } = await import('./plugins/eslint');
|
||||
plugins.push(eslint());
|
||||
}
|
||||
|
||||
if (options.lint === 'biome') {
|
||||
const { biome } = await import('./plugins/biome');
|
||||
plugins.push(biome());
|
||||
}
|
||||
|
||||
await create({
|
||||
packageManager: manager,
|
||||
template: options.template,
|
||||
outputDir: projectName,
|
||||
installDeps: options.installDeps,
|
||||
initializeGit: config.git ?? true,
|
||||
plugins,
|
||||
log: (message) => {
|
||||
info.message(message);
|
||||
return template.rename?.(file) ?? file;
|
||||
},
|
||||
});
|
||||
|
||||
info.stop('Project Generated');
|
||||
const packageJsonPath = path.join(outputDir, 'package.json');
|
||||
let packageJson = await initPackageJson(projectName, packageJsonPath);
|
||||
for (const plugin of plugins) {
|
||||
packageJson =
|
||||
(await plugin.packageJson?.call(pluginContext, packageJson)) ??
|
||||
packageJson;
|
||||
}
|
||||
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
||||
|
||||
outro(pc.bgGreen(pc.bold('Done')));
|
||||
const readmePath = path.join(outputDir, 'README.md');
|
||||
let readme = `# ${projectName}\n\n${await fs.readFile(readmePath)}`;
|
||||
for (const plugin of plugins) {
|
||||
readme = (await plugin.readme?.call(pluginContext, readme)) ?? readme;
|
||||
}
|
||||
await fs.writeFile(readmePath, readme);
|
||||
|
||||
console.log(pc.bold('\nOpen the project'));
|
||||
console.log(pc.cyan(`cd ${projectName}`));
|
||||
for (const plugin of plugins) {
|
||||
await plugin.afterWrite?.call(pluginContext);
|
||||
}
|
||||
|
||||
console.log(pc.bold('\nRun Development Server'));
|
||||
console.log(pc.cyan('npm run dev | pnpm run dev | yarn dev'));
|
||||
if (installDeps) {
|
||||
try {
|
||||
await autoInstall(packageManager, outputDir);
|
||||
log('Installed dependencies');
|
||||
} catch (err) {
|
||||
log(`Failed to install dependencies: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
pc.bold('\nYou can now open the project and start writing documents'),
|
||||
);
|
||||
|
||||
process.exit(0);
|
||||
if (initializeGit && (await tryGitInit(outputDir))) {
|
||||
log('Initialized Git repository');
|
||||
}
|
||||
}
|
||||
|
||||
program.parse();
|
||||
async function initPackageJson(
|
||||
projectName: string,
|
||||
packageJsonPath: string,
|
||||
): Promise<PackageJsonType> {
|
||||
function replaceWorkspaceDeps(deps: Record<string, string> = {}) {
|
||||
for (const k in deps) {
|
||||
if (deps[k].startsWith('workspace:') && k in depVersions) {
|
||||
deps[k] = depVersions[k as keyof typeof depVersions];
|
||||
}
|
||||
}
|
||||
|
||||
main({
|
||||
name: program.args[0],
|
||||
...program.opts(),
|
||||
}).catch((e: unknown) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
return deps;
|
||||
}
|
||||
|
||||
const packageJson: PackageJsonType = JSON.parse(
|
||||
(await fs.readFile(packageJsonPath)).toString(),
|
||||
);
|
||||
|
||||
return {
|
||||
...packageJson,
|
||||
name: projectName,
|
||||
scripts: {
|
||||
...packageJson.scripts,
|
||||
postinstall: 'fumadocs-mdx',
|
||||
},
|
||||
dependencies: replaceWorkspaceDeps(packageJson.dependencies),
|
||||
devDependencies: replaceWorkspaceDeps(packageJson.devDependencies),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TemplatePlugin } from '@/create-app';
|
||||
import { TemplatePlugin } from '@/index';
|
||||
import { copy, pick } from '@/utils';
|
||||
import path from 'node:path';
|
||||
import { depVersions, sourceDir } from '@/constants';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TemplatePlugin } from '@/create-app';
|
||||
import { TemplatePlugin } from '@/index';
|
||||
import { copy } from '@/utils';
|
||||
import path from 'node:path';
|
||||
import { depVersions, sourceDir } from '@/constants';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TemplatePlugin } from '@/create-app';
|
||||
import { TemplatePlugin } from '@/index';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ writeFileSync(
|
||||
console.log('Create-Fumadocs-App: versions.json updated');
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['./src/index.ts', './src/create-app.ts', './src/plugins/*'],
|
||||
entry: ['./src/bin.ts', './src/index.ts', './src/plugins/*'],
|
||||
format: 'esm',
|
||||
target: 'node20',
|
||||
dts: true,
|
||||
|
||||
Generated
+12
@@ -1607,6 +1607,9 @@ importers:
|
||||
specifier: ^27.0.2
|
||||
version: 27.0.2
|
||||
devDependencies:
|
||||
'@commander-js/extra-typings':
|
||||
specifier: ^14.0.0
|
||||
version: 14.0.0([email protected])
|
||||
'@types/cross-spawn':
|
||||
specifier: ^6.0.6
|
||||
version: 6.0.6
|
||||
@@ -2906,6 +2909,11 @@ packages:
|
||||
'@codemirror/[email protected]':
|
||||
resolution: {integrity: sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==}
|
||||
|
||||
'@commander-js/[email protected]':
|
||||
resolution: {integrity: sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg==}
|
||||
peerDependencies:
|
||||
commander: ~14.0.0
|
||||
|
||||
'@content-collections/[email protected]':
|
||||
resolution: {integrity: sha512-4ZbjxOjaDAxnj7mIij1q1vxZgOJQkA20ThoNZ0CsnmhJmUp+IDqIRLUyDyZ+4ZAk+zQy6bKeOzWzyLg32vMgRQ==}
|
||||
peerDependencies:
|
||||
@@ -12167,6 +12175,10 @@ snapshots:
|
||||
style-mod: 4.1.3
|
||||
w3c-keyname: 2.2.8
|
||||
|
||||
'@commander-js/[email protected]([email protected])':
|
||||
dependencies:
|
||||
commander: 14.0.1
|
||||
|
||||
'@content-collections/[email protected]([email protected])':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.0.0
|
||||
|
||||
Reference in New Issue
Block a user