@tanstack/cli
Version:
TanStack CLI
234 lines (233 loc) • 10.3 kB
JavaScript
import { intro } from '@clack/prompts';
import { finalizeAddOns, getAllAddOns, getFrameworkById, getFrameworks, getPackageManager, loadStarter, populateAddOnOptionsDefaults, readConfigFile, resolveBundler, } from '@tanstack/create';
import { getProjectName, promptForAddOnOptions, promptForEnvVars, selectAddOns, selectBundler, selectDeployment, selectExamples, selectFramework, selectGit, selectInstall, selectPackageManager, selectTemplate, selectToolchain, } from './ui-prompts.js';
import { listTemplateChoices, resolveStarterSpecifier, validateLegacyCreateFlags, } from './command-line.js';
import { resolveProjectLocation, validateProjectName, } from './utils.js';
export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], forcedDeployment, showDeploymentOptions = true, defaultFrameworkId, }) {
const options = {};
const blank = cliOptions.blank === true;
if (cliOptions.framework) {
options.framework = getFrameworkById(cliOptions.framework);
}
else {
const availableFrameworks = getFrameworks();
if (defaultFrameworkId || availableFrameworks.length <= 1) {
options.framework = getFrameworkById(defaultFrameworkId || 'react');
}
else {
options.framework = await selectFramework(availableFrameworks, defaultFrameworkId);
}
}
options.bundler = await selectBundler(options.framework, cliOptions.buildTool);
if (options.bundler === 'rsbuild') {
const validation = validateLegacyCreateFlags({
...cliOptions,
buildTool: options.bundler,
addOns: Array.isArray(cliOptions.addOns) && cliOptions.addOns.length > 0
? cliOptions.addOns
: undefined,
});
if (validation.error) {
throw new Error(validation.error);
}
if (forcedAddOns.length > 0) {
throw new Error('Rsbuild does not currently support forced add-ons.');
}
if (forcedDeployment) {
throw new Error('Rsbuild does not currently support deployments.');
}
}
const projectLocation = resolveProjectLocation({
projectName: cliOptions.projectName ?? (await getProjectName()),
targetDir: cliOptions.targetDir,
emptyProjectNameIsCurrentDirectory: true,
});
if (!projectLocation) {
throw new Error('Project name or target directory is required');
}
options.projectName = projectLocation.projectName;
options.targetDir = projectLocation.targetDir;
const { valid, error } = validateProjectName(options.projectName);
if (!valid) {
console.error(error);
process.exit(1);
}
// Mode is always file-router (TanStack Start)
options.mode = 'file-router';
const template = cliOptions.template?.toLowerCase().trim();
const isLegacyTemplate = template &&
['file-router', 'typescript', 'tsx', 'javascript', 'js', 'jsx'].includes(template);
const routerOnly = !!cliOptions.routerOnly ||
(isLegacyTemplate ? template !== 'file-router' : false);
if (!cliOptions.starter) {
if (cliOptions.template && !isLegacyTemplate) {
cliOptions.starter = cliOptions.template;
}
else if (cliOptions.templateId) {
cliOptions.starter = cliOptions.templateId;
}
}
if (options.bundler !== 'rsbuild' && !routerOnly && !blank && !cliOptions.starter) {
const starterChoices = await listTemplateChoices(options.framework.id);
const selectedTemplateId = await selectTemplate(starterChoices.map((choice) => ({
id: choice.id,
name: choice.name,
description: choice.description,
})));
if (selectedTemplateId) {
cliOptions.starter = selectedTemplateId;
}
}
const starter = options.bundler !== 'rsbuild' && !routerOnly && !blank && cliOptions.starter
? await loadStarter(await resolveStarterSpecifier(cliOptions.starter, options.framework.id))
: undefined;
if (starter) {
options.framework = getFrameworkById(starter.framework) || options.framework;
options.mode = starter.mode;
}
// TypeScript is always enabled with file-router
options.typescript = true;
// Package manager selection
if (cliOptions.packageManager) {
options.packageManager = cliOptions.packageManager;
}
else {
const detectedPackageManager = await getPackageManager();
options.packageManager =
detectedPackageManager || (await selectPackageManager());
}
// Toolchain selection
const toolchain = blank && options.bundler !== 'rsbuild' && cliOptions.toolchain === undefined
? undefined
: await selectToolchain(options.framework, cliOptions.toolchain);
// Deployment selection
let deployment;
if (routerOnly || options.bundler === 'rsbuild') {
deployment = undefined;
}
else if (cliOptions.deployment) {
deployment = cliOptions.deployment;
}
else if (blank) {
deployment = forcedDeployment;
}
else if (showDeploymentOptions) {
deployment = await selectDeployment(options.framework, cliOptions.deployment, forcedDeployment);
}
else {
deployment = forcedDeployment;
}
// Add-ons selection
const addOns = new Set();
// Examples/demo pages are enabled by default
const includeExamples = blank
? false
: (cliOptions.examples ?? (routerOnly ? false : await selectExamples()));
options.includeExamples =
includeExamples;
if (toolchain) {
addOns.add(toolchain);
}
if (deployment) {
addOns.add(deployment);
}
if (!routerOnly && options.bundler !== 'rsbuild') {
for (const addOn of starter?.dependsOn || []) {
addOns.add(addOn);
}
for (const addOn of forcedAddOns) {
addOns.add(addOn);
}
}
if (!routerOnly && options.bundler !== 'rsbuild' && Array.isArray(cliOptions.addOns)) {
for (const addOn of cliOptions.addOns) {
if (addOn.toLowerCase() === 'start') {
continue;
}
addOns.add(addOn);
}
}
else if (!routerOnly && options.bundler !== 'rsbuild' && !blank) {
for (const addOn of await selectAddOns(options.framework, options.mode, 'add-on', 'What add-ons would you like for your project?', forcedAddOns)) {
addOns.add(addOn);
}
if (includeExamples) {
for (const addOn of await selectAddOns(options.framework, options.mode, 'example', 'Would you like an example?', forcedAddOns, false)) {
addOns.add(addOn);
}
}
}
const chosenAddOns = Array.from(await finalizeAddOns(options.framework, options.mode, Array.from(addOns), options.bundler));
options.chosenAddOns = includeExamples
? chosenAddOns
: chosenAddOns.filter((addOn) => addOn.type !== 'example');
options.tailwind =
!blank || options.chosenAddOns.some((addOn) => addOn.tailwind === true);
options.projectPreset = blank ? 'blank' : 'default';
options.routerOnly = routerOnly;
// Prompt for add-on options in interactive mode
if (Array.isArray(cliOptions.addOns)) {
// Non-interactive mode: use defaults
options.addOnOptions = populateAddOnOptionsDefaults(options.chosenAddOns);
}
else {
// Interactive mode: prompt for options
const userOptions = await promptForAddOnOptions(options.chosenAddOns.map((a) => a.id), options.framework);
const defaultOptions = populateAddOnOptionsDefaults(options.chosenAddOns);
// Merge user options with defaults
options.addOnOptions = { ...defaultOptions, ...userOptions };
}
// Prompt for env vars exposed by selected add-ons in interactive mode
const envVarValues = Array.isArray(cliOptions.addOns)
? {}
: await promptForEnvVars(options.chosenAddOns);
options.envVarValues =
envVarValues;
options.git = cliOptions.git ?? (await selectGit());
options.install = cliOptions.install ?? (await selectInstall());
options.intent = cliOptions.intent ?? !blank;
if (starter) {
options.starter = starter;
}
return options;
}
export async function promptForAddOns() {
const config = await readConfigFile(process.cwd());
if (!config) {
console.error('No config file found');
process.exit(1);
}
const framework = getFrameworkById(config.framework);
if (!framework) {
console.error(`Unknown framework: ${config.framework}`);
process.exit(1);
}
intro(`Adding new add-ons to '${config.projectName}'`);
const addOns = new Set();
const bundler = resolveBundler(framework, config.bundler).id;
if (bundler === 'rsbuild') {
const toolchains = getAllAddOns(framework, config.mode, bundler).filter((addOn) => addOn.type === 'toolchain');
const configuredAddOnIds = new Set(config.chosenAddOns.map((addOn) => addOn.toLowerCase()));
const configuredExclusiveGroups = new Set(toolchains
.filter((addOn) => configuredAddOnIds.has(addOn.id.toLowerCase()))
.flatMap((addOn) => addOn.exclusive ?? []));
const unavailableToolchains = toolchains
.filter((addOn) => configuredAddOnIds.has(addOn.id.toLowerCase()) ||
addOn.exclusive?.some((exclusive) => configuredExclusiveGroups.has(exclusive)))
.map((addOn) => addOn.id);
if (unavailableToolchains.length === toolchains.length) {
return [];
}
for (const addOn of await selectAddOns(framework, config.mode, 'toolchain', 'Which toolchain would you like to add?', unavailableToolchains, false, bundler)) {
addOns.add(addOn);
}
return Array.from(addOns);
}
for (const addOn of await selectAddOns(framework, config.mode, 'add-on', 'What add-ons would you like for your project?', config.chosenAddOns)) {
addOns.add(addOn);
}
for (const addOn of await selectAddOns(framework, config.mode, 'example', 'Would you like any examples?', config.chosenAddOns)) {
addOns.add(addOn);
}
return Array.from(addOns);
}