diff --git a/.agents/index.md b/.agents/index.md index f3e7c33b..68795b27 100644 --- a/.agents/index.md +++ b/.agents/index.md @@ -14,6 +14,9 @@ npx @tanstack/cli add clerk drizzle # List available add-ons npx @tanstack/cli create --list-add-ons + +# Minimal one-route Start app +npx @tanstack/cli create my-app --blank -y ``` ## Monorepo Structure @@ -70,6 +73,7 @@ node ../cli/packages/cli/dist/index.js create my-app | `--toolchain ` | Toolchain (use `--list-add-ons` to see options) | | `--deployment ` | Deployment target (use `--list-add-ons` to see options) | | `--template ` | Use template URL/path or built-in ID | +| `--blank` | Minimal project without starter UI or default tooling | | `--no-git` | Skip git init | | `--no-install` | Skip npm install | | `-y` | Accept defaults | @@ -81,7 +85,8 @@ node ../cli/packages/cli/dist/index.js create my-app | ---------------- | ------- | ---------------------------------------- | | `projectName` | string | Project name | | `typescript` | boolean | Always true (TanStack Start requires TS) | -| `tailwind` | boolean | Always true (Tailwind always enabled) | +| `tailwind` | boolean | False for blank unless an add-on requires it | +| `blank` | boolean | True when using the blank project preset | | `fileRouter` | boolean | Always true | | `addOnEnabled` | object | `{ [id]: boolean }` | | `addOnOption` | object | `{ [id]: options }` | @@ -138,4 +143,3 @@ skills: load: "packages/cli/skills/maintain-custom-addons-dev-watch/SKILL.md" - diff --git a/.changeset/clean-cats-start.md b/.changeset/clean-cats-start.md new file mode 100644 index 00000000..3bf558df --- /dev/null +++ b/.changeset/clean-cats-start.md @@ -0,0 +1,12 @@ +--- +'@tanstack/cli': minor +'@tanstack/create': minor +--- + +Add a `--blank` project preset that creates a production-ready one-route app +without starter UI, Tailwind, devtools, test tooling, Intent setup, or unused +public assets. Pass `--intent` to opt local coding-agent skill mappings back in. +Also move integration-specific dependencies to their owning add-ons and stop +shipping an unused test stack in standard Start projects. Explicit styling and +deployment add-ons remain composable with the blank preset, including when +added later. diff --git a/README.md b/README.md index f5f3cba8..1912ae42 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,9 @@ npx @tanstack/cli create my-app # Create TanStack Start app (recommended) npx @tanstack/cli create my-app +# Create a minimal one-route Start app without selection prompts +npx @tanstack/cli create my-app --blank -y + # Create Router-only SPA (no SSR) npx @tanstack/cli create my-app --router-only diff --git a/docs/cli-reference.md b/docs/cli-reference.md index a7ad0ee8..259ae629 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -15,13 +15,14 @@ tanstack create [project-name] [options] |--------|-------------| | `--add-ons ` | Comma-separated add-on IDs | | `--template ` | Template URL/path or built-in template ID | +| `--blank` | Create a minimal one-route Start project without default starter UI, examples, Tailwind, devtools, or a test stack | | `--package-manager ` | `npm`, `pnpm`, `yarn`, `bun`, `deno` | | `--framework ` | `React`, `Solid` | | `--router-only` | Create file-based Router-only app without TanStack Start (add-ons/deployment/template disabled) | | `--toolchain ` | Toolchain add-on (use `--list-add-ons` to see options) | | `--deployment ` | Deployment add-on (use `--list-add-ons` to see options) | | `--examples` / `--no-examples` | Include or exclude demo/example pages | -| `--tailwind` / `--no-tailwind` | Deprecated compatibility flags; accepted but ignored (Tailwind is always enabled) | +| `--tailwind` / `--no-tailwind` | Deprecated compatibility flags for standard projects; blank projects omit Tailwind | | `--no-git` | Skip git init | | `--no-install` | Skip dependency install | | `-y, --yes` | Use defaults, skip prompts | @@ -36,6 +37,8 @@ tanstack create [project-name] [options] ```bash # Examples tanstack create my-app -y +tanstack create my-app --blank -y +tanstack create my-app --blank --deployment cloudflare -y tanstack create my-app --add-ons clerk,drizzle,tanstack-query tanstack create my-app --router-only --toolchain eslint --no-examples tanstack create my-app --template https://example.com/template.json @@ -44,6 +47,14 @@ tanstack create --list-add-ons --framework React --json tanstack create --addon-details drizzle --framework React --json ``` +`--blank` creates the smallest useful TanStack Start project: one route and no +default starter interface, examples, Tailwind, devtools, test dependencies, or +TanStack Intent setup. Pass `--intent` when the project should include local +skill mappings for coding agents. +Explicit add-ons and deployment adapters can add their own required files and +dependencies. Add `-y` to use defaults for every remaining option; when the +target is non-empty, also pass `--force` or the command exits without writing. + ### Programmatic generation Use `@tanstack/create/worker` in Cloudflare Workers and other edge SSR runtimes. It does not import the generated template manifest at module startup. Instead, provide a loader for the framework and add-on chunks your Worker supports. @@ -295,7 +306,7 @@ Removes `^` from version ranges for TanStack packages and adds any missing peer ## Configuration -Projects include `.tanstack.json`: +Projects include `.cta.json`: ```json { diff --git a/docs/quick-start.md b/docs/quick-start.md index a0c76782..f49fb0fd 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -17,13 +17,25 @@ Interactive prompts guide you through project name, package manager, and add-on # Defaults only (TanStack Start + file-router) npx @tanstack/cli create my-app -y +# Minimal one-route Start project +npx @tanstack/cli create my-app --blank -y + +# Minimal Start project configured for Cloudflare +npx @tanstack/cli create my-app --blank --deployment cloudflare -y + # With add-ons -npx @tanstack/cli create my-app --add-ons tanstack-query,clerk,drizzle +npx @tanstack/cli create my-app --add-ons tanstack-query,clerk,drizzle -y # Router-only SPA (no SSR) -npx @tanstack/cli create my-app --router-only +npx @tanstack/cli create my-app --router-only -y ``` +Blank projects omit the default starter interface, examples, Tailwind, +devtools, test stack, and TanStack Intent setup. Pass `--intent` to include +local skill mappings for coding agents. Explicit add-ons can add their own +required files and dependencies. Pass `-y` to skip selection prompts. A +non-empty target also requires `--force`. + ## Run the Project ```bash @@ -34,7 +46,7 @@ pnpm dev ## Environment Variables -Some add-ons require API keys. After creation: +Some add-ons require API keys. When the project includes `.env.example`: ```bash cp .env.example .env @@ -49,9 +61,9 @@ my-app/ │ ├── routes/ # File-based routing │ │ ├── __root.tsx # Root layout │ │ └── index.tsx # Home page -│ └── integrations/ # Add-on integration code -├── .tanstack.json # CLI config -└── .env.example # Required env vars +│ └── integrations/ # Present when selected add-ons need it +├── .cta.json # CLI config +└── .env.example # Present when selected add-ons need env vars ``` ## Next Steps diff --git a/examples/react/blog/package.json b/examples/react/blog/package.json index 9e6f7aa9..38e16499 100644 --- a/examples/react/blog/package.json +++ b/examples/react/blog/package.json @@ -19,7 +19,6 @@ "@tanstack/react-router-ssr-query": "^1.131.7", "@tanstack/react-start": "^1.132.0", "@tanstack/router-plugin": "^1.132.0", - "lucide-react": "^0.545.0", "react": "^19.2.0", "react-dom": "^19.2.0", "tailwindcss": "^4.1.18" diff --git a/packages/cli/skills/add-addons-existing-app/SKILL.md b/packages/cli/skills/add-addons-existing-app/SKILL.md index f11715b3..45226642 100644 --- a/packages/cli/skills/add-addons-existing-app/SKILL.md +++ b/packages/cli/skills/add-addons-existing-app/SKILL.md @@ -4,9 +4,10 @@ description: > Apply integrations to existing projects with tanstack add, including add-on id resolution, dependency chains, option prompts, and .cta.json project metadata preconditions. -type: core -library: tanstack-cli -library_version: "0.62.1" +metadata: + type: core + library: tanstack-cli + library_version: "0.62.1" --- # Add Add-ons To Existing App diff --git a/packages/cli/skills/choose-ecosystem-integrations/SKILL.md b/packages/cli/skills/choose-ecosystem-integrations/SKILL.md index 4f2ebb26..4cd53d20 100644 --- a/packages/cli/skills/choose-ecosystem-integrations/SKILL.md +++ b/packages/cli/skills/choose-ecosystem-integrations/SKILL.md @@ -5,9 +5,10 @@ description: > tanstack ecosystem --json, tanstack create --list-add-ons --json, and tanstack create --addon-details --json. Covers exclusive categories, provider options, and router-only compatibility constraints. -type: composition -library: tanstack-cli -library_version: "0.62.1" +metadata: + type: composition + library: tanstack-cli + library_version: "0.62.1" requires: - create-app-scaffold - query-docs-library-metadata diff --git a/packages/cli/skills/create-app-scaffold/SKILL.md b/packages/cli/skills/create-app-scaffold/SKILL.md index 8a2955fe..a0162ce9 100644 --- a/packages/cli/skills/create-app-scaffold/SKILL.md +++ b/packages/cli/skills/create-app-scaffold/SKILL.md @@ -2,12 +2,13 @@ name: create-app-scaffold description: > Scaffold a TanStack app with tanstack create using --framework, --template, - --toolchain, --deployment, --add-ons, and --router-only. Covers flag + --toolchain, --deployment, --add-ons, --blank, and --router-only. Covers flag compatibility, non-interactive defaults, and intent-preserving command construction. -type: core -library: tanstack-cli -library_version: "0.62.1" +metadata: + type: core + library: tanstack-cli + library_version: "0.70.0" --- # Create App Scaffold @@ -37,6 +38,20 @@ npx @tanstack/cli create acme-solid \ -y ``` +### Start from a minimal production-ready scaffold + +Use `--blank` when the task does not need the default starter UI, examples, +Tailwind, devtools, test tooling, or local Intent setup. Explicit integrations +remain composable; add `--intent` when the generated project should contain +skill mappings for later coding-agent work. + +```bash +npx @tanstack/cli create acme-web \ + --blank \ + --deployment cloudflare \ + -y +``` + ### Use router-only mode for compatibility scaffolds only ```bash @@ -75,7 +90,7 @@ In non-interactive runs, empty add-on selection can complete with defaults and s Source: https://github.com/TanStack/cli/issues/234 -### HIGH Assume --no-tailwind is still supported +### HIGH Avoid --no-tailwind; use --blank for a minimal scaffold Wrong: ```bash @@ -84,12 +99,31 @@ npx @tanstack/cli create my-app --no-tailwind -y Correct: ```bash -npx @tanstack/cli create my-app -y +npx @tanstack/cli create my-app --blank -y +``` + +`--no-tailwind` remains a deprecated compatibility flag for standard +scaffolds. `--blank` is the supported preset for a minimal project without +Tailwind. + +Source: packages/cli/src/command-line.ts:386 + +### HIGH Combine --blank with a template, examples, or --tailwind + +Wrong: +```bash +npx @tanstack/cli create my-app --blank --template ecommerce -y ``` -`--no-tailwind` is deprecated and ignored, so output still includes Tailwind and diverges from expected stack constraints. +Correct: +```bash +npx @tanstack/cli create my-app --blank --add-ons drizzle -y +``` -Source: packages/cli/src/command-line.ts:369 +Templates, examples, and `--tailwind` replace the output constraints that +`--blank` guarantees, so the CLI rejects those combinations. Deployment, +toolchain, and add-on selections remain supported; a selected styling add-on +can opt the generated app back into Tailwind when it requires it. ### CRITICAL Combine router-only with template/deployment/add-ons diff --git a/packages/cli/skills/create-app-scaffold/references/create-flag-compatibility-matrix.md b/packages/cli/skills/create-app-scaffold/references/create-flag-compatibility-matrix.md index 2a9a1aba..2b68efd0 100644 --- a/packages/cli/skills/create-app-scaffold/references/create-flag-compatibility-matrix.md +++ b/packages/cli/skills/create-app-scaffold/references/create-flag-compatibility-matrix.md @@ -1,6 +1,6 @@ # Create Flag Compatibility Matrix -Targets `@tanstack/cli` v0.62.1. +Targets `@tanstack/cli` v0.70.0. ## Compatibility @@ -12,7 +12,8 @@ Targets `@tanstack/cli` v0.62.1. | `--deployment` | yes | no | Ignored in router-only mode. | | `--template` / `--starter` | yes | no | Ignored in router-only mode. | | `--template-id` | yes | no | Ignored in router-only mode. | -| `--tailwind` / `--no-tailwind` | deprecated/ignored | deprecated/ignored | Tailwind is always enabled. | +| `--blank` | yes | yes | Omits starter UI, examples, Tailwind, devtools, and test tooling unless an explicit integration requires them. Cannot be combined with a template, `--examples`, or `--tailwind`. | +| `--tailwind` / `--no-tailwind` | deprecated/ignored | deprecated/ignored | Standard scaffolds enable Tailwind; use `--blank` for the supported minimal preset. | Source: `packages/cli/src/command-line.ts:337` @@ -29,6 +30,9 @@ Source: `packages/cli/src/command-line.ts:337` # Full scaffold preset npx @tanstack/cli create app --framework react --add-ons tanstack-query --deployment netlify -y +# Minimal Start preset +npx @tanstack/cli create app --blank --framework react --deployment cloudflare -y + # Router-only preset npx @tanstack/cli create app --router-only --framework react --toolchain biome -y ``` diff --git a/packages/cli/skills/create-app-scaffold/references/framework-adapters.md b/packages/cli/skills/create-app-scaffold/references/framework-adapters.md index c9cfc433..f932bf95 100644 --- a/packages/cli/skills/create-app-scaffold/references/framework-adapters.md +++ b/packages/cli/skills/create-app-scaffold/references/framework-adapters.md @@ -1,6 +1,6 @@ # Framework Adapter Options -Targets `@tanstack/cli` v0.61.0. +Targets `@tanstack/cli` v0.70.0. ## Supported values diff --git a/packages/cli/skills/maintain-custom-addons-dev-watch/SKILL.md b/packages/cli/skills/maintain-custom-addons-dev-watch/SKILL.md index 4ef5fac4..fdda13eb 100644 --- a/packages/cli/skills/maintain-custom-addons-dev-watch/SKILL.md +++ b/packages/cli/skills/maintain-custom-addons-dev-watch/SKILL.md @@ -4,9 +4,10 @@ description: > Build and iterate custom add-ons/templates with tanstack add-on init, add-on compile, add-on dev, and tanstack create --dev-watch, including sync loop preconditions, watch-path validation, and project metadata constraints. -type: lifecycle -library: tanstack-cli -library_version: "0.62.1" +metadata: + type: lifecycle + library: tanstack-cli + library_version: "0.62.1" requires: - add-addons-existing-app --- diff --git a/packages/cli/skills/query-docs-library-metadata/SKILL.md b/packages/cli/skills/query-docs-library-metadata/SKILL.md index 30216bf1..531e0688 100644 --- a/packages/cli/skills/query-docs-library-metadata/SKILL.md +++ b/packages/cli/skills/query-docs-library-metadata/SKILL.md @@ -4,9 +4,10 @@ description: > Retrieve machine-readable context with tanstack libraries, tanstack doc, tanstack search-docs, tanstack create --list-add-ons --json, and --addon-details for agent-safe discovery and preflight validation. -type: core -library: tanstack-cli -library_version: "0.62.1" +metadata: + type: core + library: tanstack-cli + library_version: "0.62.1" --- # Query Docs And Library Metadata diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 797561d2..ecd8bb4e 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -182,6 +182,7 @@ function getCreateTelemetryProperties(projectName: string, options: CliOptions) command_variant: getCreateCommandVariant(options), deployment: options.deployment ? sanitizeId(options.deployment) : undefined, examples: options.examples, + blank: !!options.blank, framework: options.framework ? sanitizeId(options.framework) : undefined, git: options.git, install: options.install !== false, @@ -221,6 +222,7 @@ function getResolvedCreateTelemetryProperties( add_on_ids: addOnIds, deployment: deployment ? sanitizeId(deployment.id) : undefined, examples: includeExamples, + blank: finalOptions.projectPreset === 'blank', framework: sanitizeId(finalOptions.framework.id), git: finalOptions.git, install: finalOptions.install !== false, @@ -264,6 +266,7 @@ export function cli({ async function confirmTargetDirectorySafety( targetDir: string, forced?: boolean, + nonInteractive?: boolean, ) { if (forced) { return @@ -281,6 +284,12 @@ export function cli({ return } + if (nonInteractive) { + throw new Error( + `Target directory "${targetDir}" already exists and is not empty. Pass --force to continue non-interactively.`, + ) + } + const shouldContinue = await confirm({ message: `Target directory "${targetDir}" already exists and is not empty. Continue anyway?`, initialValue: false, @@ -298,6 +307,9 @@ export function cli({ lines.push(` Location: ${finalOptions.targetDir}`) lines.push(` Framework: ${finalOptions.framework.name}`) lines.push(` Mode: ${finalOptions.mode}`) + if (finalOptions.projectPreset === 'blank') { + lines.push(` Preset: blank`) + } lines.push(` Package manager: ${finalOptions.packageManager}`) if (finalOptions.starter) { lines.push(` Template: ${finalOptions.starter.name}`) @@ -512,7 +524,11 @@ export function cli({ ) } const silentEnvironment = createUIEnvironment(appName, true, () => currentTelemetry) - await confirmTargetDirectorySafety(normalizedOpts.targetDir, options.force) + await confirmTargetDirectorySafety( + normalizedOpts.targetDir, + options.force, + !!options.nonInteractive || !!options.yes, + ) await createApp(silentEnvironment, normalizedOpts) console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`) @@ -617,6 +633,17 @@ export function cli({ addHiddenAgentFlag(program) + function validateCreateCommandOptions(options: CliOptions) { + const legacyCreateFlags = validateLegacyCreateFlags(options) + if (legacyCreateFlags.error) { + throw new Error(legacyCreateFlags.error) + } + + for (const warning of legacyCreateFlags.warnings) { + log.warn(warning) + } + } + // Helper to create the create command action handler async function handleCreate(projectName: string, options: CliOptions) { try { @@ -627,14 +654,7 @@ export function cli({ properties: getCreateTelemetryProperties(projectName, options), }, async (telemetry) => { - const legacyCreateFlags = validateLegacyCreateFlags(options) - if (legacyCreateFlags.error) { - throw new Error(legacyCreateFlags.error) - } - - for (const warning of legacyCreateFlags.warnings) { - log.warn(warning) - } + validateCreateCommandOptions(options) if (options.listAddOns) { const addOns = await getAllAddOns( @@ -895,7 +915,11 @@ export function cli({ if (cameFromPrompts) { await confirmCreateOptions(finalOptions) } - await confirmTargetDirectorySafety(finalOptions.targetDir, options.force) + await confirmTargetDirectorySafety( + finalOptions.targetDir, + options.force, + nonInteractive, + ) await createApp(environment, finalOptions) }, ) @@ -968,13 +992,17 @@ export function cli({ '--router-only', 'Use router-only compatibility mode (file-based routing without TanStack Start)', ) + .option( + '--blank', + 'Create a minimal one-route app without default starter UI, Tailwind, devtools, or tests', + ) .option( '--tailwind', - 'Deprecated: compatibility flag; Tailwind is always enabled', + 'Deprecated: compatibility flag; standard scaffolds always enable Tailwind', ) .option( '--no-tailwind', - 'Deprecated: compatibility flag; Tailwind opt-out is ignored', + 'Deprecated: compatibility flag; use --blank for a project without Tailwind', ) .option('--examples', 'include demo/example pages') .option('--no-examples', 'exclude demo/example pages') @@ -1094,6 +1122,8 @@ export function cli({ }, }, async () => { + validateCreateCommandOptions(options) + const frameworkName = options.framework || defaultFramework || 'React' const framework = getFrameworkByName(frameworkName) if (!framework) { diff --git a/packages/cli/src/command-line.ts b/packages/cli/src/command-line.ts index c375e079..7d56d9e7 100644 --- a/packages/cli/src/command-line.ts +++ b/packages/cli/src/command-line.ts @@ -321,6 +321,31 @@ export function validateLegacyCreateFlags(cliOptions: CliOptions): { const warnings: Array = [] const legacyTemplate = getLegacyTemplateValue(cliOptions.template) + if ( + cliOptions.blank && + (cliOptions.starter || cliOptions.template || cliOptions.templateId) + ) { + return { + warnings, + error: + '--blank cannot be combined with --starter, --template, or --template-id.', + } + } + + if (cliOptions.blank && cliOptions.examples === true) { + return { + warnings, + error: '--blank cannot be combined with --examples.', + } + } + + if (cliOptions.blank && cliOptions.tailwind === true) { + return { + warnings, + error: '--blank cannot be combined with --tailwind.', + } + } + if (cliOptions.starter) { warnings.push( 'The --starter flag is deprecated; prefer --template instead. Backward compatibility remains for now.', @@ -361,13 +386,13 @@ export function validateLegacyCreateFlags(cliOptions: CliOptions): { if (cliOptions.tailwind === true) { warnings.push( - 'The --tailwind flag is deprecated and ignored. Tailwind is always enabled in TanStack Start scaffolds.', + 'The --tailwind flag is deprecated and ignored. Tailwind is enabled in standard TanStack Start scaffolds.', ) } - if (cliOptions.tailwind === false) { + if (cliOptions.tailwind === false && !cliOptions.blank) { warnings.push( - 'The --no-tailwind flag is deprecated and ignored. Tailwind opt-out is intentionally unsupported to keep add-on permutations maintainable; remove Tailwind after scaffolding if needed.', + 'The --no-tailwind flag is deprecated and ignored for standard scaffolds. Use --blank to create a minimal project without Tailwind.', ) } @@ -428,6 +453,7 @@ export async function normalizeOptions( // Mode is always file-router (TanStack Start) let mode = 'file-router' let routerOnly = !!cliOptions.routerOnly + const blank = cliOptions.blank === true const legacyTemplate = getLegacyTemplateValue(cliOptions.template) @@ -450,15 +476,14 @@ export async function normalizeOptions( const preferredFramework = (cliOptions.framework || 'react').toLowerCase() - const starter = !routerOnly && cliOptions.starter + const starter = !routerOnly && !blank && cliOptions.starter ? await loadStarter( await resolveStarterSpecifier(cliOptions.starter, preferredFramework), ) : undefined - // TypeScript and Tailwind are always enabled with TanStack Start + // TypeScript is always enabled with TanStack Start. const typescript = true - const tailwind = true if (starter) { cliOptions.framework = starter.framework @@ -512,11 +537,13 @@ export async function normalizeOptions( return [] } - const includeExamples = cliOptions.examples ?? !routerOnly + const includeExamples = blank ? false : (cliOptions.examples ?? !routerOnly) const chosenAddOnsRaw = await selectAddOns() const chosenAddOns = includeExamples ? chosenAddOnsRaw : chosenAddOnsRaw.filter((addOn) => addOn.type !== 'example') + const tailwind = + !blank || chosenAddOns.some((addOn) => addOn.tailwind === true) // Handle add-on configuration option let addOnOptionsFromCLI = {} @@ -529,7 +556,7 @@ export async function normalizeOptions( } } - const normalized = { + const normalized: Options = { projectName: projectName, targetDir, framework, @@ -542,13 +569,14 @@ export async function normalizeOptions( DEFAULT_PACKAGE_MANAGER, git: cliOptions.git ?? true, install: cliOptions.install, - intent: cliOptions.intent ?? true, + intent: cliOptions.intent ?? !blank, chosenAddOns, addOnOptions: { ...populateAddOnOptionsDefaults(chosenAddOns), ...addOnOptionsFromCLI, }, starter: starter, + projectPreset: blank ? 'blank' : 'default', } ;(normalized as Options & { includeExamples?: boolean }).includeExamples = diff --git a/packages/cli/src/options.ts b/packages/cli/src/options.ts index 26cd8a3d..f09b55a2 100644 --- a/packages/cli/src/options.ts +++ b/packages/cli/src/options.ts @@ -52,6 +52,7 @@ export async function promptForCreateOptions( }, ): Promise | undefined> { const options = {} as Required + const blank = cliOptions.blank === true if (cliOptions.framework) { options.framework = getFrameworkById(cliOptions.framework)! @@ -106,7 +107,7 @@ export async function promptForCreateOptions( } } - if (!routerOnly && !cliOptions.starter) { + if (!routerOnly && !blank && !cliOptions.starter) { const starterChoices = await listTemplateChoices(options.framework.id) const selectedTemplateId = await selectTemplate( starterChoices.map((choice) => ({ @@ -120,7 +121,7 @@ export async function promptForCreateOptions( } } - const starter = !routerOnly && cliOptions.starter + const starter = !routerOnly && !blank && cliOptions.starter ? await loadStarter( await resolveStarterSpecifier(cliOptions.starter, options.framework.id), ) @@ -144,10 +145,10 @@ export async function promptForCreateOptions( } // Toolchain selection - const toolchain = await selectToolchain( - options.framework, - cliOptions.toolchain, - ) + const toolchain = + blank && cliOptions.toolchain === undefined + ? undefined + : await selectToolchain(options.framework, cliOptions.toolchain) // Deployment selection let deployment: string | undefined @@ -155,6 +156,8 @@ export async function promptForCreateOptions( deployment = undefined } else if (cliOptions.deployment) { deployment = cliOptions.deployment + } else if (blank) { + deployment = forcedDeployment } else if (showDeploymentOptions) { deployment = await selectDeployment( options.framework, @@ -169,8 +172,9 @@ export async function promptForCreateOptions( const addOns: Set = new Set() // Examples/demo pages are enabled by default - const includeExamples = - cliOptions.examples ?? (routerOnly ? false : await selectExamples()) + const includeExamples = blank + ? false + : (cliOptions.examples ?? (routerOnly ? false : await selectExamples())) ;(options as Required & { includeExamples?: boolean }).includeExamples = includeExamples @@ -197,7 +201,7 @@ export async function promptForCreateOptions( } addOns.add(addOn) } - } else if (!routerOnly) { + } else if (!routerOnly && !blank) { for (const addOn of await selectAddOns( options.framework, options.mode, @@ -229,8 +233,9 @@ export async function promptForCreateOptions( ? chosenAddOns : chosenAddOns.filter((addOn) => addOn.type !== 'example') - // Tailwind is always enabled - options.tailwind = true + options.tailwind = + !blank || options.chosenAddOns.some((addOn) => addOn.tailwind === true) + options.projectPreset = blank ? 'blank' : 'default' // Prompt for add-on options in interactive mode if (Array.isArray(cliOptions.addOns)) { @@ -256,6 +261,7 @@ export async function promptForCreateOptions( options.git = cliOptions.git ?? (await selectGit()) options.install = cliOptions.install ?? (await selectInstall()) + options.intent = cliOptions.intent ?? !blank if (starter) { options.starter = starter diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index e736cf0b..235746ae 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -24,6 +24,7 @@ export interface CliOptions { addOnConfig?: string force?: boolean routerOnly?: boolean + blank?: boolean template?: string tailwind?: boolean examples?: boolean diff --git a/packages/cli/tests-e2e/blank-smoke.spec.ts b/packages/cli/tests-e2e/blank-smoke.spec.ts new file mode 100644 index 00000000..e3f5635a --- /dev/null +++ b/packages/cli/tests-e2e/blank-smoke.spec.ts @@ -0,0 +1,254 @@ +import { execFile } from 'node:child_process' +import { + access, + mkdir, + mkdtemp, + readFile, + readdir, + rm, + writeFile, +} from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join, relative } from 'node:path' +import { promisify } from 'node:util' + +import { expect, test } from '@playwright/test' + +import { + attachRuntimeGuards, + createReactAppFixture, + getRepoPath, + optimizePageForFastE2E, +} from './helpers' + +const execFileAsync = promisify(execFile) + +const expectedDependencies = [ + '@tanstack/react-router', + '@tanstack/react-start', + 'react', + 'react-dom', +] + +const expectedDevDependencies = [ + '@cloudflare/vite-plugin', + '@tanstack/router-cli', + '@types/node', + '@types/react', + '@types/react-dom', + '@vitejs/plugin-react', + 'typescript', + 'vite', + 'wrangler', +] + +async function listFiles( + root: string, + directory = root, +): Promise> { + const files: Array = [] + + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = join(directory, entry.name) + if (entry.isDirectory()) { + files.push(...(await listFiles(root, path))) + } else { + files.push(relative(root, path).replaceAll('\\', '/')) + } + } + + return files.sort() +} + +test('@blocking refuses a non-empty target in non-interactive mode', async () => { + const rootDir = await mkdtemp(join(tmpdir(), 'tanstack-blank-collision-')) + const targetDir = join(rootDir, 'existing-app') + const sentinelPath = join(targetDir, 'keep.txt') + + try { + await mkdir(targetDir) + await writeFile(sentinelPath, 'keep me') + + let failure: unknown + try { + await execFileAsync( + process.execPath, + [ + getRepoPath('packages/cli/dist/index.js'), + 'create', + 'existing-app', + '--blank', + '-y', + '--no-install', + '--no-git', + ], + { cwd: rootDir, encoding: 'utf8' }, + ) + } catch (error) { + failure = error + } + + const commandError = failure as + | (Error & { code?: number; stdout?: string; stderr?: string }) + | undefined + expect(commandError?.code).toBe(1) + expect( + `${commandError?.stdout ?? ''}\n${commandError?.stderr ?? ''}`, + ).toContain('Pass --force to continue non-interactively') + await expect(readFile(sentinelPath, 'utf8')).resolves.toBe('keep me') + } finally { + await rm(rootDir, { recursive: true, force: true }) + } +}) + +test('@blocking rejects conflicting blank flags for dev', async () => { + const rootDir = await mkdtemp(join(tmpdir(), 'tanstack-blank-dev-conflict-')) + const appName = 'invalid-blank-dev-app' + + try { + let failure: unknown + try { + await execFileAsync( + process.execPath, + [ + getRepoPath('packages/cli/dist/index.js'), + 'dev', + appName, + '--blank', + '--tailwind', + '-y', + '--no-install', + '--no-git', + ], + { cwd: rootDir, encoding: 'utf8' }, + ) + } catch (error) { + failure = error + } + + const commandError = failure as + | (Error & { code?: number; stdout?: string; stderr?: string }) + | undefined + expect(commandError?.code).toBe(1) + expect( + `${commandError?.stdout ?? ''}\n${commandError?.stderr ?? ''}`, + ).toContain('--blank cannot be combined with --tailwind') + await expect(access(join(rootDir, appName))).rejects.toThrow() + } finally { + await rm(rootDir, { recursive: true, force: true }) + } +}) + +test('@blocking cleans up a fixture when setup fails', async () => { + let failedRootDir: string | undefined + + await expect( + createReactAppFixture({ + appName: 'failed-blank-fixture', + blank: true, + packageManager: 'pnpm', + skipDevServer: true, + afterCreate: async (appDir) => { + failedRootDir = dirname(appDir) + await access(join(appDir, 'node_modules')) + throw new Error('fixture setup sentinel') + }, + }), + ).rejects.toThrow('fixture setup sentinel') + + expect(failedRootDir).toBeDefined() + await expect(access(failedRootDir!)).rejects.toThrow() +}) + +test('@blocking creates a production-valid blank React app', async ({ + page, +}) => { + const fixture = await createReactAppFixture({ + appName: 'react-blank-smoke-app', + blank: true, + deployment: 'cloudflare', + packageManager: 'npm', + runQualityGatesChecks: true, + afterCreate: async (appDir) => { + await access(join(appDir, 'package-lock.json')) + await access(join(appDir, 'node_modules')) + await access(join(appDir, 'wrangler.jsonc')) + + await expect( + readFile(join(appDir, 'README.md'), 'utf8'), + ).resolves.toContain('Deploy to Cloudflare Workers') + + const routeTreePath = join(appDir, 'src/routeTree.gen.ts') + await access(routeTreePath) + await expect(readFile(routeTreePath, 'utf8')).resolves.toContain( + "from './routes/index'", + ) + + expect(await listFiles(join(appDir, 'src'))).toEqual([ + 'routeTree.gen.ts', + 'router.tsx', + 'routes/__root.tsx', + 'routes/index.tsx', + 'styles.css', + ]) + + const packageJSON = JSON.parse( + await readFile(join(appDir, 'package.json'), 'utf8'), + ) as { + scripts?: Record + dependencies?: Record + devDependencies?: Record + } + + expect(Object.keys(packageJSON.dependencies ?? {}).sort()).toEqual( + expectedDependencies, + ) + expect(Object.keys(packageJSON.devDependencies ?? {}).sort()).toEqual( + expectedDevDependencies, + ) + expect(packageJSON.scripts).toEqual({ + dev: 'vite dev --port 3000', + 'generate-routes': 'tsr generate', + build: 'vite build', + preview: 'vite preview', + deploy: 'npm run build && wrangler deploy', + }) + + for (const excludedPath of [ + 'index.html', + 'public', + 'src/App.test.tsx', + 'src/components', + 'src/main.tsx', + 'src/routes/about.tsx', + ]) { + await expect(access(join(appDir, excludedPath))).rejects.toThrow() + } + }, + }) + const guards = attachRuntimeGuards(page, fixture.url) + + try { + const response = await fetch(fixture.url) + expect(response.status).toBe(200) + expect(response.headers.get('content-type')).toContain('text/html') + + const html = await response.text() + expect(html).toContain('Welcome to TanStack Start') + expect(html).toContain('src/routes/index.tsx') + + await optimizePageForFastE2E(page) + await page.goto(fixture.url) + await expect( + page.getByRole('heading', { name: 'Welcome to TanStack Start' }), + ).toBeVisible() + } finally { + try { + guards.assertClean() + } finally { + guards.dispose() + await fixture.stop() + await fixture.cleanup() + } + } +}) diff --git a/packages/cli/tests-e2e/helpers.ts b/packages/cli/tests-e2e/helpers.ts index f85ac0c5..091ed028 100644 --- a/packages/cli/tests-e2e/helpers.ts +++ b/packages/cli/tests-e2e/helpers.ts @@ -23,10 +23,13 @@ type CreateAppFixtureOptions = { appName: string framework?: 'react' | 'solid' packageManager?: 'pnpm' | 'npm' | 'yarn' | 'bun' | 'deno' + blank?: boolean + deployment?: string routerOnly?: boolean template?: string addOns?: Array postCreateAddOns?: Array + afterCreate?: (appDir: string) => Promise skipDevServer?: boolean runQualityGatesChecks?: boolean } @@ -349,8 +352,11 @@ export async function createAppFixture( const rootDir = await mkdtemp(join(tmpdir(), 'tanstack-cli-e2e-')) const { appName, + blank = false, + deployment, template, addOns, + afterCreate, postCreateAddOns, skipDevServer, runQualityGatesChecks = false, @@ -359,6 +365,9 @@ export async function createAppFixture( routerOnly = false, } = options const appDir = join(rootDir, appName) + const cleanup = async () => { + await rm(rootDir, { recursive: true, force: true }) + } const createArgs = [ cliDistPath, @@ -375,6 +384,14 @@ export async function createAppFixture( createArgs.push('--router-only') } + if (blank) { + createArgs.push('--blank', '--yes', '--no-intent') + } + + if (deployment) { + createArgs.push('--deployment', deployment) + } + if (template) { createArgs.push('--template', template) } @@ -383,39 +400,46 @@ export async function createAppFixture( createArgs.push('--add-ons', addOns.join(',')) } - const createStartedAt = now() - await runCommand( - 'node', - createArgs, - { - cwd: rootDir, - env: { - CI: '1', + try { + const createStartedAt = now() + await runCommand( + 'node', + createArgs, + { + cwd: rootDir, + env: { + CI: '1', + }, }, - }, - ) - mark('create', createStartedAt) + ) + mark('create', createStartedAt) - await patchViteConfigForE2E(appDir) + await afterCreate?.(appDir) - if (postCreateAddOns?.length) { - const postAddOnsStartedAt = now() - await runCommand('node', [cliDistPath, 'add', ...postCreateAddOns], { - cwd: appDir, - env: { - CI: '1', - }, - }) + await patchViteConfigForE2E(appDir) - mark('post-add-ons', postAddOnsStartedAt) + if (postCreateAddOns?.length) { + const postAddOnsStartedAt = now() + await runCommand('node', [cliDistPath, 'add', ...postCreateAddOns], { + cwd: appDir, + env: { + CI: '1', + }, + }) - await patchViteConfigForE2E(appDir) - } + mark('post-add-ons', postAddOnsStartedAt) - if (runQualityGatesChecks) { - const qualityGatesStartedAt = now() - await runQualityGates(appDir, packageManager) - mark('quality-gates', qualityGatesStartedAt) + await patchViteConfigForE2E(appDir) + } + + if (runQualityGatesChecks) { + const qualityGatesStartedAt = now() + await runQualityGates(appDir, packageManager) + mark('quality-gates', qualityGatesStartedAt) + } + } catch (error) { + await cleanup().catch(() => undefined) + throw error } if (skipDevServer) { @@ -427,9 +451,7 @@ export async function createAppFixture( framework, packageManager, stop: async () => {}, - cleanup: async () => { - await rm(rootDir, { recursive: true, force: true }) - }, + cleanup, } } @@ -463,6 +485,7 @@ export async function createAppFixture( mark('dev-server-ready', devServerStartedAt) } catch (error) { await stopChild(server) + await cleanup().catch(() => undefined) throw new Error( `Failed to start app server at ${url}\nstdout:\n${serverStdout}\n\nstderr:\n${serverStderr}\n\n${error}`, ) @@ -479,9 +502,7 @@ export async function createAppFixture( stop: async () => { await stopChild(server) }, - cleanup: async () => { - await rm(rootDir, { recursive: true, force: true }) - }, + cleanup, } } diff --git a/packages/cli/tests/command-line.test.ts b/packages/cli/tests/command-line.test.ts index 4842b7f3..e859b8bc 100644 --- a/packages/cli/tests/command-line.test.ts +++ b/packages/cli/tests/command-line.test.ts @@ -588,6 +588,61 @@ describe('normalizeOptions', () => { expect((options as any)?.includeExamples).toBe(false) }) + it('normalizes --blank to the blank project preset', async () => { + const options = await normalizeOptions({ + projectName: 'test', + blank: true, + }) + + expect(options?.projectPreset).toBe('blank') + expect(options?.includeExamples).toBe(false) + expect(options?.tailwind).toBe(false) + expect(options?.intent).toBe(false) + }) + + it('keeps explicitly selected integrations in the blank preset', async () => { + __testRegisterFramework({ + id: 'react', + name: 'react', + getAddOns: () => [ + { + id: 'biome', + name: 'Biome', + modes: ['file-router'], + type: 'toolchain', + }, + { + id: 'cloudflare', + name: 'Cloudflare', + modes: ['file-router'], + type: 'deployment', + }, + { + id: 'tanstack-query', + name: 'TanStack Query', + modes: ['file-router'], + type: 'add-on', + }, + ], + }) + + const options = await normalizeOptions({ + projectName: 'test', + framework: 'react', + blank: true, + toolchain: 'biome', + deployment: 'cloudflare', + addOns: ['tanstack-query'], + }) + + expect(options?.projectPreset).toBe('blank') + expect(options?.chosenAddOns.map((addOn) => addOn.id).sort()).toEqual([ + 'biome', + 'cloudflare', + 'tanstack-query', + ]) + }) + it('should ignore add-ons and deployment in router-only mode but keep toolchain', async () => { __testRegisterFramework({ id: 'react', @@ -668,6 +723,25 @@ describe('validateLegacyCreateFlags', () => { expect(result.error).toBeUndefined() }) + it('accepts --blank by itself', () => { + const result = validateLegacyCreateFlags({ blank: true }) + expect(result.warnings).toEqual([]) + expect(result.error).toBeUndefined() + }) + + it.each([ + [{ starter: 'ecommerce' }, '--starter'], + [{ template: 'ecommerce' }, '--template'], + [{ templateId: 'ecommerce' }, '--template-id'], + [{ examples: true }, '--examples'], + [{ tailwind: true }, '--tailwind'], + ])('rejects --blank with conflicting flags containing %s', (flags, name) => { + const result = validateLegacyCreateFlags({ blank: true, ...flags }) + + expect(result.error).toContain('--blank') + expect(result.error).toContain(name) + }) + it('warns when --router-only is used', () => { const result = validateLegacyCreateFlags({ routerOnly: true }) expect(result.error).toBeUndefined() @@ -684,7 +758,7 @@ describe('validateLegacyCreateFlags', () => { const result = validateLegacyCreateFlags({ tailwind: false }) expect(result.error).toBeUndefined() expect(result.warnings[0]).toContain('--no-tailwind') - expect(result.warnings[0]).toContain('intentionally unsupported') + expect(result.warnings[0]).toContain('--blank') }) it('errors for JavaScript templates', () => { diff --git a/packages/cli/tests/options.test.ts b/packages/cli/tests/options.test.ts index f6fb8fa0..826af6dd 100644 --- a/packages/cli/tests/options.test.ts +++ b/packages/cli/tests/options.test.ts @@ -205,6 +205,25 @@ describe('promptForCreateOptions', () => { expect(prompts.selectTemplate).not.toHaveBeenCalled() }) + it('uses the blank preset without prompting for optional scaffold features', async () => { + setBasicSpies() + + const options = await promptForCreateOptions( + { ...baseCliOptions, blank: true }, + {}, + ) + + expect(options?.projectPreset).toBe('blank') + expect(options?.includeExamples).toBe(false) + expect(options?.tailwind).toBe(false) + expect(options?.intent).toBe(false) + expect(prompts.selectTemplate).not.toHaveBeenCalled() + expect(prompts.selectExamples).not.toHaveBeenCalled() + expect(prompts.selectToolchain).not.toHaveBeenCalled() + expect(prompts.selectDeployment).not.toHaveBeenCalled() + expect(prompts.selectAddOns).not.toHaveBeenCalled() + }) + //// Package manager it('uses the package manager from the cli options', async () => { diff --git a/packages/create/package.json b/packages/create/package.json index 8ce84896..01ecd0dc 100644 --- a/packages/create/package.json +++ b/packages/create/package.json @@ -42,7 +42,7 @@ "scripts": { "build": "npm run generate-manifest && tsc && npm run copy-assets", "generate-manifest": "node ./scripts/generate-manifest.mjs", - "copy-assets": "node -e \"const fs=require('fs');const path=require('path');function copyDir(src,dest){if(!fs.existsSync(dest))fs.mkdirSync(dest,{recursive:true});for(const entry of fs.readdirSync(src,{withFileTypes:true})){const srcPath=path.join(src,entry.name);const destPath=path.join(dest,entry.name);if(entry.isDirectory())copyDir(srcPath,destPath);else fs.copyFileSync(srcPath,destPath)}}['react','solid'].forEach(fw=>{['add-ons','toolchains','hosts','examples','project'].forEach(dir=>{const src='src/frameworks/'+fw+'/'+dir;const dest='dist/frameworks/'+fw+'/'+dir;if(fs.existsSync(src))copyDir(src,dest)})})\"", + "copy-assets": "node ./scripts/copy-assets.mjs", "dev": "npm run generate-manifest && tsc --watch", "test": "npm run generate-manifest && eslint ./src && vitest run", "test:watch": "npm run generate-manifest && vitest", diff --git a/packages/create/scripts/copy-assets.mjs b/packages/create/scripts/copy-assets.mjs new file mode 100644 index 00000000..bbcbd6bb --- /dev/null +++ b/packages/create/scripts/copy-assets.mjs @@ -0,0 +1,35 @@ +import { cpSync, existsSync, rmSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const frameworks = ['react', 'solid'] +const assetDirectories = [ + 'add-ons', + 'toolchains', + 'hosts', + 'examples', + 'project', +] + +for (const framework of frameworks) { + for (const directory of assetDirectories) { + const source = resolve( + packageRoot, + 'src/frameworks', + framework, + directory, + ) + const destination = resolve( + packageRoot, + 'dist/frameworks', + framework, + directory, + ) + + rmSync(destination, { recursive: true, force: true }) + if (existsSync(source)) { + cpSync(source, destination, { recursive: true }) + } + } +} diff --git a/packages/create/scripts/generate-manifest.mjs b/packages/create/scripts/generate-manifest.mjs index de87ed0d..fdba7e85 100644 --- a/packages/create/scripts/generate-manifest.mjs +++ b/packages/create/scripts/generate-manifest.mjs @@ -266,6 +266,7 @@ function createTemplateRendererSource(renderers = templateRenderers) { projectName, typescript, tailwind, + blank, js, jsx, fileRouter, @@ -317,6 +318,7 @@ type TemplateRenderContext = { projectName: any typescript: any tailwind: any + blank: any js: any jsx: any fileRouter: any diff --git a/packages/create/src/add-to-app.ts b/packages/create/src/add-to-app.ts index af4e224a..8349160e 100644 --- a/packages/create/src/add-to-app.ts +++ b/packages/create/src/add-to-app.ts @@ -43,16 +43,19 @@ async function createOptions( const framework = getFrameworkById(json.framework) const starter = json.starter ? await loadStarter(json.starter) : undefined + const chosenAddOns = await finalizeAddOns(framework!, json.mode!, [ + ...json.chosenAddOns, + ...addOns, + ]) return { ...json, framework, - tailwind: true, + tailwind: + (json.tailwind ?? true) || + chosenAddOns.some((addOn) => addOn.tailwind === true), addOns: true, - chosenAddOns: await finalizeAddOns(framework!, json.mode!, [ - ...json.chosenAddOns, - ...addOns, - ]), + chosenAddOns, targetDir, starter, intent: json.intent ?? false, diff --git a/packages/create/src/create-app.ts b/packages/create/src/create-app.ts index 46add011..acb4f970 100644 --- a/packages/create/src/create-app.ts +++ b/packages/create/src/create-app.ts @@ -19,7 +19,10 @@ import { runSpecialSteps } from './special-steps/index.js' import type { Environment, FileBundleHandler, Options } from './types.js' function stripExamplesFromOptions(options: Options): Options { - if (options.includeExamples !== false) { + const includeExamples = + options.projectPreset !== 'blank' && options.includeExamples !== false + + if (includeExamples) { return options } @@ -53,6 +56,7 @@ function stripExamplesFromOptions(options: Options): Options { return { ...options, + includeExamples: false, chosenAddOns, } } @@ -451,6 +455,10 @@ ${environment.getErrors().join('\n')}` ` const nextSteps = buildNextSteps(options) + const readmeDescription = + options.projectPreset === 'blank' + ? 'running, building, adding routes, and selected integrations.' + : 'testing, styling, adding routes, etc.' // Use the force luke! :) environment.outro( @@ -461,7 +469,7 @@ ${cdInstruction}% ${formatCommand( getPackageManagerScriptCommand(options.packageManager, ['dev']), )} ${nextSteps} -Please read the README.md file for information on testing, styling, adding routes, etc.${errorStatement}`, +Please read the README.md file for information on ${readmeDescription}${errorStatement}`, ) } diff --git a/packages/create/src/edge-create-app.ts b/packages/create/src/edge-create-app.ts index 77d3d65d..624ccdfd 100644 --- a/packages/create/src/edge-create-app.ts +++ b/packages/create/src/edge-create-app.ts @@ -15,7 +15,10 @@ import { import type { Environment, FileBundleHandler, Options } from './types.js' function stripExamplesFromOptions(options: Options): Options { - if (options.includeExamples !== false) { + const includeExamples = + options.projectPreset !== 'blank' && options.includeExamples !== false + + if (includeExamples) { return options } @@ -49,6 +52,7 @@ function stripExamplesFromOptions(options: Options): Options { return { ...options, + includeExamples: false, chosenAddOns, } } @@ -567,6 +571,10 @@ ${environment.getErrors().join('\n')}` ` const nextSteps = buildNextSteps(options) + const readmeDescription = + options.projectPreset === 'blank' + ? 'running, building, adding routes, and selected integrations.' + : 'testing, styling, adding routes, etc.' environment.outro( `${locationMessage} @@ -576,7 +584,7 @@ ${cdInstruction}% ${formatCommand( getPackageManagerScriptCommand(options.packageManager, ['dev']), )} ${nextSteps} -Please read the README.md file for information on testing, styling, adding routes, etc.${errorStatement}`, +Please read the README.md file for information on ${readmeDescription}${errorStatement}`, ) } diff --git a/packages/create/src/edge-package-json.ts b/packages/create/src/edge-package-json.ts index bdc9704a..56174e0c 100644 --- a/packages/create/src/edge-package-json.ts +++ b/packages/create/src/edge-package-json.ts @@ -96,6 +96,14 @@ export function mergePackageJSON( export function createPackageJSON(options: Options) { const packageManager = options.packageManager + const blank = options.projectPreset === 'blank' + const includeDevtools = + !blank || + options.chosenAddOns.some((addOn) => + addOn.integrations?.some( + (integration) => integration.type === 'devtools', + ), + ) function getPackageManagerExecuteScript( pkg: string, @@ -113,7 +121,13 @@ export function createPackageJSON(options: Options) { const additions: Array | undefined> = [ options.framework.optionalPackages.typescript, - options.framework.optionalPackages.tailwindcss, + includeDevtools ? options.framework.optionalPackages.devtools : undefined, + !blank && options.includeExamples !== false + ? options.framework.optionalPackages.examples + : undefined, + options.tailwind + ? options.framework.optionalPackages.tailwindcss + : undefined, options.mode ? options.framework.optionalPackages[options.mode] : undefined, ] for (const addition of additions.filter( @@ -132,7 +146,8 @@ export function createPackageJSON(options: Options) { packageManager: options.packageManager, projectName: options.projectName, typescript: true, - tailwind: true, + tailwind: options.tailwind, + blank, js: 'ts', jsx: 'tsx', fileRouter: options.mode === 'file-router', diff --git a/packages/create/src/edge-render.ts b/packages/create/src/edge-render.ts index db47fa10..6133bf9f 100644 --- a/packages/create/src/edge-render.ts +++ b/packages/create/src/edge-render.ts @@ -9,6 +9,7 @@ export type TemplateRenderContext = { projectName: string | undefined typescript: boolean | undefined tailwind: boolean | undefined + blank: boolean | undefined js: string | undefined jsx: string | undefined fileRouter: boolean | undefined @@ -92,6 +93,7 @@ function renderWithRenderer( projectName: undefined, typescript: undefined, tailwind: undefined, + blank: undefined, js: undefined, jsx: undefined, fileRouter: undefined, diff --git a/packages/create/src/edge-template-file.ts b/packages/create/src/edge-template-file.ts index bf7d67de..916a2342 100644 --- a/packages/create/src/edge-template-file.ts +++ b/packages/create/src/edge-template-file.ts @@ -128,13 +128,15 @@ export function createTemplateFile(environment: Environment, options: Options) { packageManager: options.packageManager, projectName: options.projectName, typescript: true, - tailwind: true, + tailwind: options.tailwind, + blank: options.projectPreset === 'blank', js: 'ts', jsx: 'tsx', fileRouter: options.mode === 'file-router', codeRouter: options.mode === 'code-router', routerOnly: options.routerOnly === true, - includeExamples: options.includeExamples !== false, + includeExamples: + options.projectPreset !== 'blank' && options.includeExamples !== false, addOnEnabled, addOnOption: options.addOnOptions, addOns: options.chosenAddOns, diff --git a/packages/create/src/frameworks/react/add-ons/posthog/info.json b/packages/create/src/frameworks/react/add-ons/posthog/info.json index fad5e154..de375e95 100644 --- a/packages/create/src/frameworks/react/add-ons/posthog/info.json +++ b/packages/create/src/frameworks/react/add-ons/posthog/info.json @@ -8,7 +8,6 @@ "color": "#1D4AFF", "priority": 20, "link": "https://posthog.com", - "tailwind": true, "routes": [ { "icon": "BarChart", diff --git a/packages/create/src/frameworks/react/add-ons/shadcn/info.json b/packages/create/src/frameworks/react/add-ons/shadcn/info.json index 958b2d2b..923029c9 100644 --- a/packages/create/src/frameworks/react/add-ons/shadcn/info.json +++ b/packages/create/src/frameworks/react/add-ons/shadcn/info.json @@ -6,6 +6,7 @@ "link": "https://ui.shadcn.com/", "type": "add-on", "category": "styling", + "tailwind": true, "color": "#000000", "priority": 30 } diff --git a/packages/create/src/frameworks/react/add-ons/shadcn/package.json b/packages/create/src/frameworks/react/add-ons/shadcn/package.json index ed2a10d9..d913c699 100644 --- a/packages/create/src/frameworks/react/add-ons/shadcn/package.json +++ b/packages/create/src/frameworks/react/add-ons/shadcn/package.json @@ -5,5 +5,8 @@ "lucide-react": "^0.577.0", "tailwind-merge": "^3.0.2", "tw-animate-css": "^1.3.6" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.16" } } diff --git a/packages/create/src/frameworks/react/add-ons/shopify/info.json b/packages/create/src/frameworks/react/add-ons/shopify/info.json index 4647ddac..e037764d 100644 --- a/packages/create/src/frameworks/react/add-ons/shopify/info.json +++ b/packages/create/src/frameworks/react/add-ons/shopify/info.json @@ -5,6 +5,7 @@ "modes": ["file-router"], "type": "add-on", "category": "other", + "tailwind": true, "color": "#5A31F4", "priority": 80, "link": "https://shopify.dev/docs/storefronts/headless", diff --git a/packages/create/src/frameworks/react/add-ons/strapi/package.json b/packages/create/src/frameworks/react/add-ons/strapi/package.json index 91573589..a8966a5c 100644 --- a/packages/create/src/frameworks/react/add-ons/strapi/package.json +++ b/packages/create/src/frameworks/react/add-ons/strapi/package.json @@ -1,6 +1,7 @@ { "dependencies": { "@strapi/client": "^1.6.1", + "lucide-react": "^0.577.0", "react-markdown": "^9.0.1", "remark-gfm": "^4.0.0", "use-debounce": "^10.1.0" diff --git a/packages/create/src/frameworks/react/add-ons/tanstack-query/package.json b/packages/create/src/frameworks/react/add-ons/tanstack-query/package.json index 1210c53d..b3f749ad 100644 --- a/packages/create/src/frameworks/react/add-ons/tanstack-query/package.json +++ b/packages/create/src/frameworks/react/add-ons/tanstack-query/package.json @@ -1,6 +1,7 @@ { "dependencies": { "@tanstack/react-query": "latest", - "@tanstack/react-query-devtools": "latest" + "@tanstack/react-query-devtools": "latest", + "@tanstack/react-router-ssr-query": "latest" } } diff --git a/packages/create/src/frameworks/react/examples/events/package.json b/packages/create/src/frameworks/react/examples/events/package.json index 75464d5d..fe624d33 100644 --- a/packages/create/src/frameworks/react/examples/events/package.json +++ b/packages/create/src/frameworks/react/examples/events/package.json @@ -10,6 +10,7 @@ "@tanstack/store": "latest", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "lucide-react": "^0.577.0", "marked": "^15.0.8", "streamdown": "^2.3.0", "tailwind-merge": "^3.0.2", diff --git a/packages/create/src/frameworks/react/examples/resume/package.json b/packages/create/src/frameworks/react/examples/resume/package.json index 6e5bd767..4b845ca6 100644 --- a/packages/create/src/frameworks/react/examples/resume/package.json +++ b/packages/create/src/frameworks/react/examples/resume/package.json @@ -14,6 +14,7 @@ "@tanstack/store": "latest", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "lucide-react": "^0.577.0", "marked": "^15.0.8", "streamdown": "^2.3.0", "tailwind-merge": "^3.0.2", diff --git a/packages/create/src/frameworks/react/hosts/cloudflare/package.json.ejs b/packages/create/src/frameworks/react/hosts/cloudflare/package.json.ejs index 83540cb5..5a15c454 100644 --- a/packages/create/src/frameworks/react/hosts/cloudflare/package.json.ejs +++ b/packages/create/src/frameworks/react/hosts/cloudflare/package.json.ejs @@ -2,10 +2,8 @@ "scripts": { "deploy": "<%= packageManager %> run build && wrangler deploy" }, - "dependencies": { - "@cloudflare/vite-plugin": "^1.26.0" - }, "devDependencies": { + "@cloudflare/vite-plugin": "^1.26.0", "wrangler": "^4.70.0" } } diff --git a/packages/create/src/frameworks/react/project/base/README.md.ejs b/packages/create/src/frameworks/react/project/base/README.md.ejs index ebdcd6aa..b1e1d980 100644 --- a/packages/create/src/frameworks/react/project/base/README.md.ejs +++ b/packages/create/src/frameworks/react/project/base/README.md.ejs @@ -1,28 +1,42 @@ -Welcome to your new TanStack Start app! +<% if (blank) { -%> +# <%= projectName %> -# Getting Started - -To run this application: +A minimal TanStack Start app with one route and <%= tailwind ? 'Tailwind CSS' : 'plain CSS' %>. ```bash <%= packageManager %> install <%= getPackageManagerRunScript('dev') %> ``` -# Building For Production +Edit `src/routes/index.tsx` to get started. Add route files under +`src/routes`; TanStack Router updates `src/routeTree.gen.ts` for you. -To build this application for production: +Build the production app with: ```bash <%= getPackageManagerRunScript('build') %> ``` +<% for(const addon of addOns.filter(addon => addon.readme)) { %> +<%- addon.readmeIsEjs ? renderTemplate(addon.readme) : addon.readme %> +<% } %> +<% } else { -%> +Welcome to your new TanStack Start app! -## Testing +# Getting Started -This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with: +To run this application: ```bash -<%= getPackageManagerRunScript('test') %> +<%= packageManager %> install +<%= getPackageManagerRunScript('dev') %> +``` + +# Building For Production + +To build this application for production: + +```bash +<%= getPackageManagerRunScript('build') %> ``` ## Styling @@ -36,7 +50,7 @@ If you prefer not to use Tailwind CSS: 1. Remove the demo pages in `src/routes/demo/` 2. Replace the Tailwind import in `src/styles.css` with your own styles 3. Remove `tailwindcss()` from the plugins array in `vite.config.ts` -4. Uninstall the packages: `<%= getPackageManagerAddScript('@tailwindcss/vite tailwindcss', true) %>` +4. Remove `@tailwindcss/vite` and `tailwindcss` from `package.json` <% if (addOnEnabled.biome || addOnEnabled.eslint) { %> ## Linting & Formatting <% if (addOnEnabled.biome) { %> @@ -197,12 +211,15 @@ function PeopleComponent() { Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters). +<% if (includeExamples) { %> # Demo files Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed. +<% } %> # Learn More You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com). For TanStack Start specific documentation, visit [TanStack Start](https://tanstack.com/start). +<% } -%> diff --git a/packages/create/src/frameworks/react/project/base/package.json b/packages/create/src/frameworks/react/project/base/package.json index 139568dd..0b33ecb1 100644 --- a/packages/create/src/frameworks/react/project/base/package.json +++ b/packages/create/src/frameworks/react/project/base/package.json @@ -9,31 +9,17 @@ "dev": "vite dev --port 3000", "generate-routes": "tsr generate", "build": "vite build", - "preview": "vite preview", - "test": "vitest run" + "preview": "vite preview" }, "dependencies": { - "@tanstack/react-devtools": "latest", "@tanstack/react-router": "latest", - "@tanstack/react-router-devtools": "latest", - "@tanstack/react-router-ssr-query": "latest", "@tanstack/react-start": "latest", - "lucide-react": "^0.577.0", "react": "^19.2.0", "react-dom": "^19.2.0" }, "devDependencies": { - "@tailwindcss/typography": "^0.5.16", - "@tanstack/devtools-vite": "latest", - "@testing-library/dom": "^10.4.1", - "@testing-library/react": "^16.3.0", - "@types/react": "^19.2.0", - "@types/react-dom": "^19.2.0", "@vitejs/plugin-react": "^6.0.1", - "jsdom": "^28.1.0", - "typescript": "^6.0.2", - "vite": "^8.0.0", - "vitest": "^4.1.5" + "vite": "^8.0.0" }, "pnpm": { "onlyBuiltDependencies": [ diff --git a/packages/create/src/frameworks/react/project/base/public/favicon.ico b/packages/create/src/frameworks/react/project/base/public/favicon.ico deleted file mode 100644 index a11777cc..00000000 Binary files a/packages/create/src/frameworks/react/project/base/public/favicon.ico and /dev/null differ diff --git a/packages/create/src/frameworks/react/project/base/public/logo192.png b/packages/create/src/frameworks/react/project/base/public/logo192.png deleted file mode 100644 index fc44b0a3..00000000 Binary files a/packages/create/src/frameworks/react/project/base/public/logo192.png and /dev/null differ diff --git a/packages/create/src/frameworks/react/project/base/public/logo512.png b/packages/create/src/frameworks/react/project/base/public/logo512.png deleted file mode 100644 index a4e47a65..00000000 Binary files a/packages/create/src/frameworks/react/project/base/public/logo512.png and /dev/null differ diff --git a/packages/create/src/frameworks/react/project/base/public/manifest.json b/packages/create/src/frameworks/react/project/base/public/manifest.json deleted file mode 100644 index 078ef501..00000000 --- a/packages/create/src/frameworks/react/project/base/public/manifest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "short_name": "TanStack App", - "name": "Create TanStack App Sample", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} diff --git a/packages/create/src/frameworks/react/project/base/public/robots.txt b/packages/create/src/frameworks/react/project/base/public/robots.txt deleted file mode 100644 index e9e57dc4..00000000 --- a/packages/create/src/frameworks/react/project/base/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: diff --git a/packages/create/src/frameworks/react/project/base/src/App.test.tsx.ejs b/packages/create/src/frameworks/react/project/base/src/App.test.tsx.ejs deleted file mode 100644 index e210abeb..00000000 --- a/packages/create/src/frameworks/react/project/base/src/App.test.tsx.ejs +++ /dev/null @@ -1,10 +0,0 @@ -<% if (fileRouter) { ignoreFile() } %>import { describe, expect, test } from "vitest"; -import { render, screen } from "@testing-library/react"; -import App from "./App.<%= jsx %>"; - -describe("App", () => { - test("renders", () => { - render(); - expect(screen.getByText("Learn React")).toBeDefined(); - }); -}); diff --git a/packages/create/src/frameworks/react/project/base/src/routes/__root.tsx.ejs b/packages/create/src/frameworks/react/project/base/src/routes/__root.tsx.ejs index 2b3098ad..aa7f0e11 100644 --- a/packages/create/src/frameworks/react/project/base/src/routes/__root.tsx.ejs +++ b/packages/create/src/frameworks/react/project/base/src/routes/__root.tsx.ejs @@ -1,7 +1,10 @@ +<% const showDevtools = !blank || integrations.some((integration) => integration.type === 'devtools') -%> <% if (routerOnly) { %> import { Outlet, createRootRoute } from '@tanstack/react-router' +<% if (showDevtools) { %> import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' import { TanStackDevtools } from '@tanstack/react-devtools' +<% } %> import '../styles.css' @@ -13,7 +16,7 @@ function RootComponent() { return ( <> - , }, ]} - /> + /><% } %> ) } -<% } else if (!includeExamples) { %> +<% } else if (!includeExamples || blank) { %> <% let hasContext = addOnEnabled["apollo-client"] || addOnEnabled["tanstack-query"]; %> import { HeadContent, Scripts, <% if (hasContext) { %>createRootRouteWithContext<% } else { %>createRootRoute<% } %> } from '@tanstack/react-router' -import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'; +<% if (showDevtools) { %>import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'; import { TanStackDevtools } from '@tanstack/react-devtools' +<% } %> <% for(const integration of integrations.filter(i => i.type === 'layout' || i.type === 'provider' || i.type === 'devtools')) { %> import <%= integration.jsName %> from '<%= relativePath(integration.path, true) %>' <% } %><% if (addOnEnabled.paraglide) { %> @@ -101,7 +105,7 @@ function RootDocument({ children }: { children: React.ReactNode }) { <% for(const integration of integrations.filter(i => i.type === 'provider')) { %><<%= integration.jsName %>> <% } %>{children} - i.type === 'devtools')) { %><%= integration.jsName %>,<% } %> ]} - /> + /><% } %> <% for(const integration of integrations.filter(i => i.type === 'layout')) { %><<%= integration.jsName %> /> <% } %><% for(const integration of integrations.filter(i => i.type === 'provider').reverse()) { %>> <% } %> diff --git a/packages/create/src/frameworks/react/project/base/src/routes/index.tsx.ejs b/packages/create/src/frameworks/react/project/base/src/routes/index.tsx.ejs index 7dd740be..10ad1b88 100644 --- a/packages/create/src/frameworks/react/project/base/src/routes/index.tsx.ejs +++ b/packages/create/src/frameworks/react/project/base/src/routes/index.tsx.ejs @@ -1,4 +1,19 @@ -<% if (!includeExamples) { %> +<% if (blank) { %> +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ component: Home }) + +function Home() { + return ( +
+

Welcome to TanStack Start

+

+ Edit src/routes/index.tsx to get started. +

+
+ ) +} +<% } else if (!includeExamples) { %> import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/")({ component: Home }); diff --git a/packages/create/src/frameworks/react/project/base/src/styles.css.ejs b/packages/create/src/frameworks/react/project/base/src/styles.css.ejs index ceb9069c..54a185ff 100644 --- a/packages/create/src/frameworks/react/project/base/src/styles.css.ejs +++ b/packages/create/src/frameworks/react/project/base/src/styles.css.ejs @@ -1,4 +1,21 @@ -<% if (!includeExamples) { %> +<% if (blank) { %> +<% if (tailwind) { %>@import "tailwindcss"; + +<% } %> +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: system-ui, sans-serif; +} + +main { + width: min(42rem, calc(100% - 2rem)); + margin: 4rem auto; +} +<% } else if (!includeExamples) { %> @import "tailwindcss"; * { diff --git a/packages/create/src/frameworks/react/project/base/vite.config.ts.ejs b/packages/create/src/frameworks/react/project/base/vite.config.ts.ejs index 3aedff14..0a60f457 100644 --- a/packages/create/src/frameworks/react/project/base/vite.config.ts.ejs +++ b/packages/create/src/frameworks/react/project/base/vite.config.ts.ejs @@ -1,5 +1,8 @@ import { defineConfig } from 'vite' +<% const showDevtools = !blank || integrations.some((integration) => integration.type === 'devtools') -%> +<% if (showDevtools) { -%> import { devtools } from '@tanstack/devtools-vite' +<% } -%> <% if (addOnEnabled.paraglide) { -%> import { paraglideVitePlugin } from "@inlang/paraglide-js" <% } -%> @@ -14,18 +17,20 @@ import babel from '@rolldown/plugin-babel' <% } else { -%> import viteReact from '@vitejs/plugin-react' <% } -%> +<% if (tailwind) { -%> import tailwindcss from "@tailwindcss/vite" +<% } -%> <% for(const integration of integrations.filter(i => i.type === 'vite-plugin')) { %><%- integrationImportContent(integration) %> <% } %> const config = defineConfig({ resolve: { tsconfigPaths: true }, - plugins: [devtools(), <% if (addOnEnabled.paraglide) { %>paraglideVitePlugin({ + plugins: [<% if (showDevtools) { %>devtools(), <% } %><% if (addOnEnabled.paraglide) { %>paraglideVitePlugin({ project: './project.inlang', outdir: './src/paraglide', strategy: ['url', "baseLocale"], }), <% } %><% for(const integration of integrations.filter(i => i.type === 'vite-plugin')) { %><%- integrationImportCode(integration) %>,<% } %> - tailwindcss(), + <% if (tailwind) { %>tailwindcss(),<% } %> <% if (routerOnly) { %>tanstackRouter({ target: 'react', autoCodeSplitting: true }),<% } else { %>tanstackStart(),<% } %> viteReact(),<% if (addOnEnabled.compiler) { %> babel({ presets: [reactCompilerPreset()] }),<% } %> diff --git a/packages/create/src/frameworks/react/project/packages.json b/packages/create/src/frameworks/react/project/packages.json index 2bc6816a..5103bfc7 100644 --- a/packages/create/src/frameworks/react/project/packages.json +++ b/packages/create/src/frameworks/react/project/packages.json @@ -10,16 +10,26 @@ "tailwindcss": { "dependencies": { "@tailwindcss/vite": "^4.1.18", - "tailwindcss": "^4.1.18", - "lucide-react": "^0.545.0" + "tailwindcss": "^4.1.18" + } + }, + "devtools": { + "dependencies": { + "@tanstack/react-devtools": "latest", + "@tanstack/react-router-devtools": "latest" + }, + "devDependencies": { + "@tanstack/devtools-vite": "latest" + } + }, + "examples": { + "devDependencies": { + "@tailwindcss/typography": "^0.5.16" } }, "file-router": { "devDependencies": { "@tanstack/router-cli": "^1.132.0" - }, - "dependencies": { - "@tanstack/router-plugin": "^1.132.0" } } } diff --git a/packages/create/src/frameworks/solid/add-ons/solid-ui/info.json b/packages/create/src/frameworks/solid/add-ons/solid-ui/info.json index 242b5b58..3b5f282f 100644 --- a/packages/create/src/frameworks/solid/add-ons/solid-ui/info.json +++ b/packages/create/src/frameworks/solid/add-ons/solid-ui/info.json @@ -6,5 +6,6 @@ "modes": ["file-router", "code-router"], "type": "add-on", "category": "styling", + "tailwind": true, "color": "#000000" } diff --git a/packages/create/src/frameworks/solid/project/base/README.md.ejs b/packages/create/src/frameworks/solid/project/base/README.md.ejs index c4aabe61..f45a0219 100644 --- a/packages/create/src/frameworks/solid/project/base/README.md.ejs +++ b/packages/create/src/frameworks/solid/project/base/README.md.ejs @@ -1,4 +1,26 @@ -Welcome to your new TanStack Start app! +<% if (blank) { -%> +# <%= projectName %> + +A minimal TanStack Start app with one route and <%= tailwind ? 'Tailwind CSS' : 'plain CSS' %>. + +```bash +<%= packageManager %> install +<%= getPackageManagerRunScript('dev') %> +``` + +Edit `src/routes/index.tsx` to get started. Add route files under +`src/routes`; TanStack Router updates `src/routeTree.gen.ts` for you. + +Build the production app with: + +```bash +<%= getPackageManagerRunScript('build') %> +``` +<% for(const addon of addOns.filter(addon => addon.readme)) { %> +<%- addon.readmeIsEjs ? renderTemplate(addon.readme) : addon.readme %> +<% } %> +<% } else { -%> +Welcome to your new TanStack Start app! # Getting Started @@ -28,7 +50,7 @@ If you prefer not to use Tailwind CSS: 1. Remove the demo pages in `src/routes/demo/` 2. Replace the Tailwind import in `src/styles.css` with your own styles 3. Remove `tailwindcss()` from the plugins array in `vite.config.ts` -4. Uninstall the packages: `<%= getPackageManagerAddScript('@tailwindcss/vite tailwindcss', true) %>` +4. Remove `@tailwindcss/vite` and `tailwindcss` from `package.json` <% for(const addon of addOns.filter(addon => addon.readme)) { %> <%- addon.readmeIsEjs ? renderTemplate(addon.readme) : addon.readme %> @@ -115,9 +137,11 @@ function PeopleComponent() { Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/solid/guide/data-loading#loader-parameters). +<% if (includeExamples) { %> # Demo files Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed. +<% } %> <% if (addOnEnabled.biome || addOnEnabled.eslint) { %> ## Linting & Formatting @@ -139,3 +163,4 @@ This project uses [eslint](https://eslint.org/) and [prettier](https://prettier. You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com). For TanStack Start specific documentation, visit [TanStack Start](https://tanstack.com/start). +<% } -%> diff --git a/packages/create/src/frameworks/solid/project/base/_dot_cursorrules.append b/packages/create/src/frameworks/solid/project/base/_dot_cursorrules.append deleted file mode 100644 index e859e810..00000000 --- a/packages/create/src/frameworks/solid/project/base/_dot_cursorrules.append +++ /dev/null @@ -1,35 +0,0 @@ -// Solid.js with Tailwind CSS .cursorrules - -// Prefer functional components - -const preferFunctionalComponents = true; - -// Solid.js and Tailwind CSS best practices - -const solidjsTailwindBestPractices = [ -"Use createSignal() for reactive state", -"Implement Tailwind CSS classes for styling", -"Utilize TypeScript's strict mode", -"Utilize @apply directive in CSS files for reusable styles", -"Implement responsive design using Tailwind's responsive classes", -"Use Tailwind's CSS in /src/styles.css for global styles", -"Implement dark mode using Tailwind's dark variant", -]; - -// Additional instructions - -const additionalInstructions = ` - -1. Use .tsx extension for files with JSX -2. Implement strict TypeScript checks -3. Implement proper Tailwind CSS purging for production builds -4. Utilize TanStack Router for routing when applicable -5. Use type-safe context with createContext -6. Implement proper typing for event handlers -7. Follow TypeScript best practices and naming conventions -8. Use type assertions sparingly and only when necessary -9. Use Tailwind's @layer directive for custom styles -10. Implement utility-first CSS approach -11. Follow both Solid.js and Tailwind naming conventions -12. Use JIT (Just-In-Time) mode for faster development - `; diff --git a/packages/create/src/frameworks/solid/project/base/package.json b/packages/create/src/frameworks/solid/project/base/package.json index 5e653255..0c5afcac 100644 --- a/packages/create/src/frameworks/solid/project/base/package.json +++ b/packages/create/src/frameworks/solid/project/base/package.json @@ -7,26 +7,21 @@ "generate-routes": "tsr generate", "build": "vite build", "start": "node .output/server/index.mjs", - "preview": "vite preview", - "test": "vitest run" + "preview": "vite preview" }, "dependencies": { "@tanstack/solid-router": "latest", - "@tanstack/solid-router-devtools": "latest", - "@tanstack/solid-router-ssr-query": "latest", "@tanstack/solid-start": "latest", "solid-js": "^1.9.12" }, "devDependencies": { - "@solidjs/testing-library": "^0.8.10", - "@tanstack/devtools-vite": "latest", - "jsdom": "^28.1.0", "vite-plugin-solid": "^2.11.12", - "typescript": "^6.0.2", - "vite": "^8.0.0", - "vitest": "^4.1.5" + "vite": "^8.0.0" }, "pnpm": { - "onlyBuiltDependencies": ["esbuild", "lightningcss"] + "onlyBuiltDependencies": [ + "esbuild", + "lightningcss" + ] } } diff --git a/packages/create/src/frameworks/solid/project/base/public/favicon.ico b/packages/create/src/frameworks/solid/project/base/public/favicon.ico deleted file mode 100644 index a11777cc..00000000 Binary files a/packages/create/src/frameworks/solid/project/base/public/favicon.ico and /dev/null differ diff --git a/packages/create/src/frameworks/solid/project/base/public/logo192.png b/packages/create/src/frameworks/solid/project/base/public/logo192.png deleted file mode 100644 index fc44b0a3..00000000 Binary files a/packages/create/src/frameworks/solid/project/base/public/logo192.png and /dev/null differ diff --git a/packages/create/src/frameworks/solid/project/base/public/logo512.png b/packages/create/src/frameworks/solid/project/base/public/logo512.png deleted file mode 100644 index a4e47a65..00000000 Binary files a/packages/create/src/frameworks/solid/project/base/public/logo512.png and /dev/null differ diff --git a/packages/create/src/frameworks/solid/project/base/public/manifest.json b/packages/create/src/frameworks/solid/project/base/public/manifest.json deleted file mode 100644 index 078ef501..00000000 --- a/packages/create/src/frameworks/solid/project/base/public/manifest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "short_name": "TanStack App", - "name": "Create TanStack App Sample", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} diff --git a/packages/create/src/frameworks/solid/project/base/public/robots.txt b/packages/create/src/frameworks/solid/project/base/public/robots.txt deleted file mode 100644 index e9e57dc4..00000000 --- a/packages/create/src/frameworks/solid/project/base/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: diff --git a/packages/create/src/frameworks/solid/project/base/src/App.test.tsx.ejs b/packages/create/src/frameworks/solid/project/base/src/App.test.tsx.ejs deleted file mode 100644 index 7a28f0aa..00000000 --- a/packages/create/src/frameworks/solid/project/base/src/App.test.tsx.ejs +++ /dev/null @@ -1,10 +0,0 @@ -<% if (fileRouter) { ignoreFile() } %>import { describe, expect, test } from "vitest"; -import { render, screen } from "@solidjs/testing-library"; -import App from "./App.<%= jsx %>"; - -describe("App", () => { - test("renders", () => { - render(() => ); - expect(screen.getByText("Learn Solid")).toBeDefined(); - }); -}); diff --git a/packages/create/src/frameworks/solid/project/base/src/routes/__root.tsx.ejs b/packages/create/src/frameworks/solid/project/base/src/routes/__root.tsx.ejs index 949f34df..55513063 100644 --- a/packages/create/src/frameworks/solid/project/base/src/routes/__root.tsx.ejs +++ b/packages/create/src/frameworks/solid/project/base/src/routes/__root.tsx.ejs @@ -1,6 +1,9 @@ +<% const showDevtools = !blank || integrations.some((integration) => integration.type === 'devtools') -%> <% if (routerOnly) { %> import { Outlet, createRootRoute } from '@tanstack/solid-router' +<% if (showDevtools) { %> import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools' +<% } %> import '../styles.css' @@ -12,13 +15,15 @@ function RootComponent() { return ( <> - + <% if (showDevtools) { %><% } %> ) } -<% } else if (!includeExamples) { %> +<% } else if (!includeExamples || blank) { %> import { HeadContent, Outlet, Scripts, createRootRouteWithContext } from '@tanstack/solid-router' +<% if (showDevtools) { %> import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools' +<% } %> <% if (addOnEnabled['solid-ui']) { %> import "@fontsource/inter/400.css" @@ -51,7 +56,7 @@ function RootComponent() { - + <% if (showDevtools) { %><% } %> <% for(const integration of integrations.filter(i => i.type === 'layout')) { %> <<%= integration.jsName %> /> <% } %> diff --git a/packages/create/src/frameworks/solid/project/base/src/routes/index.tsx.ejs b/packages/create/src/frameworks/solid/project/base/src/routes/index.tsx.ejs index dc0e5446..098c692f 100644 --- a/packages/create/src/frameworks/solid/project/base/src/routes/index.tsx.ejs +++ b/packages/create/src/frameworks/solid/project/base/src/routes/index.tsx.ejs @@ -1,4 +1,19 @@ -<% if (!includeExamples) { %> +<% if (blank) { %> +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ component: Home }) + +function Home() { + return ( +
+

Welcome to TanStack Start

+

+ Edit src/routes/index.tsx to get started. +

+
+ ) +} +<% } else if (!includeExamples) { %> import { createFileRoute } from '@tanstack/solid-router' export const Route = createFileRoute('/')({ component: Home }) diff --git a/packages/create/src/frameworks/solid/project/base/src/styles.css.ejs b/packages/create/src/frameworks/solid/project/base/src/styles.css.ejs index 9bbdd0f4..c26f38be 100644 --- a/packages/create/src/frameworks/solid/project/base/src/styles.css.ejs +++ b/packages/create/src/frameworks/solid/project/base/src/styles.css.ejs @@ -1,4 +1,21 @@ -<% if (!includeExamples) { %> +<% if (blank) { %> +<% if (tailwind) { %>@import 'tailwindcss'; + +<% } %> +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: system-ui, sans-serif; +} + +main { + width: min(42rem, calc(100% - 2rem)); + margin: 4rem auto; +} +<% } else if (!includeExamples) { %> @import 'tailwindcss'; * { diff --git a/packages/create/src/frameworks/solid/project/base/vite.config.ts.ejs b/packages/create/src/frameworks/solid/project/base/vite.config.ts.ejs index 5fca2364..875ffc02 100644 --- a/packages/create/src/frameworks/solid/project/base/vite.config.ts.ejs +++ b/packages/create/src/frameworks/solid/project/base/vite.config.ts.ejs @@ -1,6 +1,11 @@ import { defineConfig } from "vite"; +<% const showDevtools = !blank || integrations.some((integration) => integration.type === 'devtools') -%> +<% if (showDevtools) { -%> import { devtools } from '@tanstack/devtools-vite' +<% } -%> +<% if (tailwind) { -%> import tailwindcss from "@tailwindcss/vite" +<% } -%> <% if (routerOnly) { %> import { tanstackRouter } from '@tanstack/router-plugin/vite' <% } else { %> @@ -12,8 +17,8 @@ import solidPlugin from 'vite-plugin-solid'; export default defineConfig({ resolve: { tsconfigPaths: true }, - plugins: [devtools(), <% for(const integration of integrations.filter(i => i.type === 'vite-plugin')) { %><%- integrationImportCode(integration) %>,<% } %> - tailwindcss(), + plugins: [<% if (showDevtools) { %>devtools(), <% } %><% for(const integration of integrations.filter(i => i.type === 'vite-plugin')) { %><%- integrationImportCode(integration) %>,<% } %> + <% if (tailwind) { %>tailwindcss(),<% } %> <% if (routerOnly) { %>tanstackRouter({ target: 'solid', autoCodeSplitting: true }),<% } else { %>tanstackStart(),<% } %> solidPlugin(<% if (!routerOnly) { %>{ ssr: true }<% } %>), ], diff --git a/packages/create/src/frameworks/solid/project/packages.json b/packages/create/src/frameworks/solid/project/packages.json index 603bd779..a8f76660 100644 --- a/packages/create/src/frameworks/solid/project/packages.json +++ b/packages/create/src/frameworks/solid/project/packages.json @@ -10,12 +10,17 @@ "tailwindcss": "^4.1.18" } }, + "devtools": { + "dependencies": { + "@tanstack/solid-router-devtools": "latest" + }, + "devDependencies": { + "@tanstack/devtools-vite": "latest" + } + }, "file-router": { "devDependencies": { "@tanstack/router-cli": "^1.133.21" - }, - "dependencies": { - "@tanstack/router-plugin": "^1.133.21" } } } diff --git a/packages/create/src/package-json.ts b/packages/create/src/package-json.ts index a929a487..892d5094 100644 --- a/packages/create/src/package-json.ts +++ b/packages/create/src/package-json.ts @@ -58,6 +58,14 @@ export function mergePackageJSON( export function createPackageJSON(options: Options) { const packageManager = options.packageManager + const blank = options.projectPreset === 'blank' + const includeDevtools = + !blank || + options.chosenAddOns.some((addOn) => + addOn.integrations?.some( + (integration) => integration.type === 'devtools', + ), + ) function getPackageManagerExecuteScript( pkg: string, @@ -73,7 +81,13 @@ export function createPackageJSON(options: Options) { const additions: Array | undefined> = [ options.framework.optionalPackages.typescript, - options.framework.optionalPackages.tailwindcss, + includeDevtools ? options.framework.optionalPackages.devtools : undefined, + !blank && options.includeExamples !== false + ? options.framework.optionalPackages.examples + : undefined, + options.tailwind + ? options.framework.optionalPackages.tailwindcss + : undefined, options.mode ? options.framework.optionalPackages[options.mode] : undefined, ] for (const addition of additions.filter(Boolean)) { @@ -89,7 +103,8 @@ export function createPackageJSON(options: Options) { packageManager: options.packageManager, projectName: options.projectName, typescript: true, - tailwind: true, + tailwind: options.tailwind, + blank, js: 'ts', jsx: 'tsx', fileRouter: options.mode === 'file-router', diff --git a/packages/create/src/template-file.ts b/packages/create/src/template-file.ts index 520a4d23..8713c127 100644 --- a/packages/create/src/template-file.ts +++ b/packages/create/src/template-file.ts @@ -131,13 +131,15 @@ export function createTemplateFile(environment: Environment, options: Options) { packageManager: options.packageManager, projectName: options.projectName, typescript: true, - tailwind: true, + tailwind: options.tailwind, + blank: options.projectPreset === 'blank', js: 'ts', jsx: 'tsx', fileRouter: options.mode === 'file-router', codeRouter: options.mode === 'code-router', routerOnly: options.routerOnly === true, - includeExamples: options.includeExamples !== false, + includeExamples: + options.projectPreset !== 'blank' && options.includeExamples !== false, addOnEnabled, addOnOption: options.addOnOptions, addOns: options.chosenAddOns, diff --git a/packages/create/src/types.ts b/packages/create/src/types.ts index 23721d57..20dcc2c1 100644 --- a/packages/create/src/types.ts +++ b/packages/create/src/types.ts @@ -228,6 +228,7 @@ export interface Options { chosenAddOns: Array addOnOptions: Record> starter?: Starter | undefined + projectPreset?: 'default' | 'blank' routerOnly?: boolean includeExamples?: boolean envVarValues?: Record diff --git a/packages/create/tests/add-to-app.test.ts b/packages/create/tests/add-to-app.test.ts index 07252cb9..e3d9e423 100644 --- a/packages/create/tests/add-to-app.test.ts +++ b/packages/create/tests/add-to-app.test.ts @@ -61,6 +61,7 @@ beforeEach(() => { type: 'add-on', phase: 'add-on', modes: ['code-router', 'file-router'], + tailwind: true, command: { command: 'echo', args: ['baz'], @@ -391,4 +392,34 @@ describe('addToApp', () => { ), }) }) + + it('enables Tailwind when an add-on requires it', async () => { + const { environment } = createMemoryEnvironment('/foo') + environment.startRun() + environment.writeFile( + '/foo/.cta.json', + JSON.stringify({ ...fakeCTAJSON, tailwind: false }, null, 2), + ) + environment.writeFile( + '/foo/package.json', + JSON.stringify( + { + name: 'test', + version: '1.0.0', + scripts: {}, + dependencies: {}, + devDependencies: {}, + }, + null, + 2, + ), + ) + + await addToApp(environment, ['test'], '/foo', { + forced: true, + }) + + const persisted = JSON.parse(await environment.readFile('/foo/.cta.json')) + expect(persisted.tailwind).toBe(true) + }) }) diff --git a/packages/create/tests/blank-preset.test.ts b/packages/create/tests/blank-preset.test.ts new file mode 100644 index 00000000..22909adf --- /dev/null +++ b/packages/create/tests/blank-preset.test.ts @@ -0,0 +1,319 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { + finalizeAddOns, + populateAddOnOptionsDefaults, +} from '../src/add-ons.js' +import { createApp } from '../src/create-app.js' +import { createMemoryEnvironment } from '../src/environment.js' +import { createPackageJSON as createEdgePackageJSON } from '../src/edge-package-json.js' +import { createFrameworkDefinition as createReactFrameworkDefinition } from '../src/frameworks/react/index.js' +import { createFrameworkDefinition as createSolidFrameworkDefinition } from '../src/frameworks/solid/index.js' +import { createPackageJSON as createNodePackageJSON } from '../src/package-json.js' + +import type { Framework, FrameworkDefinition, Options } from '../src/types.js' + +const BLANK_FILE_ALLOWLIST = [ + '.cta.json', + '.gitignore', + '.vscode/settings.json', + 'README.md', + 'package.json', + 'src/router.tsx', + 'src/routes/__root.tsx', + 'src/routes/index.tsx', + 'src/styles.css', + 'tsconfig.json', + 'tsr.config.json', + 'vite.config.ts', +] + +const BLANK_DEPENDENCY_ALLOWLIST = [ + '@tanstack/react-router', + '@tanstack/react-start', + 'react', + 'react-dom', +] + +const BLANK_DEV_DEPENDENCY_ALLOWLIST = [ + '@tanstack/router-cli', + '@types/node', + '@types/react', + '@types/react-dom', + '@vitejs/plugin-react', + 'typescript', + 'vite', +] + +function frameworkFromDefinition(definition: FrameworkDefinition): Framework { + const { addOns, base, ...framework } = definition + + return { + ...framework, + getFiles: () => Promise.resolve(Object.keys(base)), + getFileContents: (path: string) => Promise.resolve(base[path]), + getDeletedFiles: () => Promise.resolve([]), + getAddOns: () => addOns, + } +} + +function createBlankOptions(framework: Framework): Options { + return { + projectName: 'blank-app', + targetDir: '/blank-app', + framework, + mode: 'file-router', + typescript: true, + tailwind: false, + packageManager: 'npm', + git: false, + install: false, + intent: false, + chosenAddOns: [], + addOnOptions: {}, + projectPreset: 'blank', + } +} + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe('blank project preset', () => { + it('renders only the blank file allowlist', async () => { + vi.stubGlobal( + 'fetch', + vi.fn( + async () => + new Response(JSON.stringify({ version: '1.0.0' }), { status: 200 }), + ), + ) + + const framework = frameworkFromDefinition(createReactFrameworkDefinition()) + const options = createBlankOptions(framework) + const { environment, output } = createMemoryEnvironment() + + await createApp(environment, options) + + const files = Object.fromEntries( + Object.entries(output.files).map(([path, contents]) => [ + path.slice(`${options.targetDir}/`.length), + contents, + ]), + ) + + expect(Object.keys(files).sort()).toEqual(BLANK_FILE_ALLOWLIST) + + const config = JSON.parse(files['.cta.json']) + expect(config.projectPreset).toBe('blank') + expect(config.tailwind).toBe(false) + + expect(files['src/routes/__root.tsx']).not.toMatch( + /TanStack(?:Router)?Devtools/, + ) + expect(files['src/router.tsx']).not.toContain( + 'setupRouterSsrQueryIntegration', + ) + expect(files['src/styles.css']).not.toContain('tailwindcss') + expect(files['vite.config.ts']).not.toMatch(/devtools|tailwindcss/) + expect(files['README.md']).not.toMatch(/Tailwind CSS|Vitest/) + expect(files['README.md']).toContain('plain CSS') + }) + + it('uses only blank dependencies and keeps Node and edge composition equal', () => { + const framework = frameworkFromDefinition(createReactFrameworkDefinition()) + const options = createBlankOptions(framework) + + const nodePackageJSON = createNodePackageJSON(options) + const edgePackageJSON = createEdgePackageJSON(options) + + expect(edgePackageJSON).toEqual(nodePackageJSON) + expect(Object.keys(nodePackageJSON.scripts).sort()).toEqual([ + 'build', + 'dev', + 'generate-routes', + 'preview', + ]) + expect(Object.keys(nodePackageJSON.dependencies).sort()).toEqual( + BLANK_DEPENDENCY_ALLOWLIST, + ) + expect(Object.keys(nodePackageJSON.devDependencies).sort()).toEqual( + BLANK_DEV_DEPENDENCY_ALLOWLIST, + ) + }) + + it('keeps the Solid blank preset equally lean', async () => { + vi.stubGlobal( + 'fetch', + vi.fn( + async () => + new Response(JSON.stringify({ version: '1.0.0' }), { status: 200 }), + ), + ) + + const framework = frameworkFromDefinition(createSolidFrameworkDefinition()) + const options = createBlankOptions(framework) + const { environment, output } = createMemoryEnvironment() + + await createApp(environment, options) + + const files = Object.keys(output.files) + .map((path) => path.slice(`${options.targetDir}/`.length)) + .sort() + const packageJSON = createNodePackageJSON(options) + + expect(files).toEqual(BLANK_FILE_ALLOWLIST) + expect(Object.keys(packageJSON.dependencies).sort()).toEqual([ + '@tanstack/solid-router', + '@tanstack/solid-start', + 'solid-js', + ]) + expect(Object.keys(packageJSON.devDependencies).sort()).toEqual([ + '@tanstack/router-cli', + 'typescript', + 'vite', + 'vite-plugin-solid', + ]) + expect(output.files['/blank-app/README.md']).toContain('plain CSS') + }) + + it('lets an explicit styling add-on opt back into Tailwind', async () => { + vi.stubGlobal( + 'fetch', + vi.fn( + async () => + new Response(JSON.stringify({ version: '1.0.0' }), { status: 200 }), + ), + ) + + const framework = frameworkFromDefinition(createReactFrameworkDefinition()) + const chosenAddOns = await finalizeAddOns(framework, 'file-router', [ + 'shadcn', + ]) + const options = { + ...createBlankOptions(framework), + tailwind: chosenAddOns.some((addOn) => addOn.tailwind === true), + chosenAddOns, + addOnOptions: populateAddOnOptionsDefaults(chosenAddOns), + } + const { environment, output } = createMemoryEnvironment() + + await createApp(environment, options) + + const packageJSON = createNodePackageJSON(options) + expect(options.tailwind).toBe(true) + expect(packageJSON.dependencies).toMatchObject({ + '@tailwindcss/vite': '^4.1.18', + tailwindcss: '^4.1.18', + }) + expect(packageJSON.devDependencies).toMatchObject({ + '@tailwindcss/typography': '^0.5.16', + }) + expect(output.files['/blank-app/src/styles.css']).toContain( + "@plugin '@tailwindcss/typography'", + ) + expect(output.files['/blank-app/vite.config.ts']).toContain('tailwindcss()') + expect(output.files['/blank-app/README.md']).toContain('Tailwind CSS') + expect(output.files['/blank-app/README.md']).not.toContain('plain CSS') + }) + + it('describes Tailwind accurately for a Solid styling add-on', async () => { + vi.stubGlobal( + 'fetch', + vi.fn( + async () => + new Response(JSON.stringify({ version: '1.0.0' }), { status: 200 }), + ), + ) + + const framework = frameworkFromDefinition(createSolidFrameworkDefinition()) + const chosenAddOns = await finalizeAddOns(framework, 'file-router', [ + 'solid-ui', + ]) + const options = { + ...createBlankOptions(framework), + tailwind: chosenAddOns.some((addOn) => addOn.tailwind === true), + chosenAddOns, + addOnOptions: populateAddOnOptionsDefaults(chosenAddOns), + } + const { environment, output } = createMemoryEnvironment() + + await createApp(environment, options) + + expect(options.tailwind).toBe(true) + expect(output.files['/blank-app/README.md']).toContain('Tailwind CSS') + expect(output.files['/blank-app/README.md']).not.toContain('plain CSS') + }) + + it('keeps Shopify storefront styling in the blank preset', async () => { + vi.stubGlobal( + 'fetch', + vi.fn( + async () => + new Response(JSON.stringify({ version: '1.0.0' }), { status: 200 }), + ), + ) + + const framework = frameworkFromDefinition(createReactFrameworkDefinition()) + const chosenAddOns = await finalizeAddOns(framework, 'file-router', [ + 'shopify', + ]) + const options = { + ...createBlankOptions(framework), + tailwind: chosenAddOns.some((addOn) => addOn.tailwind === true), + chosenAddOns, + addOnOptions: populateAddOnOptionsDefaults(chosenAddOns), + } + const { environment, output } = createMemoryEnvironment() + + await createApp(environment, options) + + const packageJSON = createNodePackageJSON(options) + expect(options.tailwind).toBe(true) + expect(packageJSON.dependencies).toMatchObject({ + '@tailwindcss/vite': '^4.1.18', + tailwindcss: '^4.1.18', + }) + expect(output.files['/blank-app/src/styles.css']).toContain( + '@import "tailwindcss"', + ) + expect(output.files['/blank-app/vite.config.ts']).toContain('tailwindcss()') + expect(output.files['/blank-app/src/routes/shop.index.tsx']).toContain( + 'tracking-tight', + ) + }) + + it('does not keep Tailwind for a stripped add-on demo', async () => { + vi.stubGlobal( + 'fetch', + vi.fn( + async () => + new Response(JSON.stringify({ version: '1.0.0' }), { status: 200 }), + ), + ) + + const framework = frameworkFromDefinition(createReactFrameworkDefinition()) + const chosenAddOns = await finalizeAddOns(framework, 'file-router', [ + 'posthog', + ]) + const options = { + ...createBlankOptions(framework), + tailwind: chosenAddOns.some((addOn) => addOn.tailwind === true), + chosenAddOns, + } + const { environment, output } = createMemoryEnvironment() + + await createApp(environment, options) + + const packageJSON = createNodePackageJSON(options) + expect(options.tailwind).toBe(false) + expect(packageJSON.dependencies).not.toHaveProperty('@tailwindcss/vite') + expect(packageJSON.dependencies).not.toHaveProperty('tailwindcss') + expect(output.files['/blank-app/src/styles.css']).not.toContain( + 'tailwindcss', + ) + expect(output.files).not.toHaveProperty( + '/blank-app/src/routes/demo/posthog.tsx', + ) + }) +}) diff --git a/packages/create/tests/package-json.test.ts b/packages/create/tests/package-json.test.ts index f9e05306..e2d96a06 100644 --- a/packages/create/tests/package-json.test.ts +++ b/packages/create/tests/package-json.test.ts @@ -1,10 +1,70 @@ import { describe, expect, it } from 'vitest' +import { createPackageJSON as createEdgePackageJSON } from '../src/edge-package-json.js' import { createPackageJSON } from '../src/package-json.js' import type { Options, Framework } from '../src/types.js' describe('createPackageJSON', () => { + it.each([ + ['react', '@tanstack/react-start'], + ['solid', '@tanstack/solid-start'], + ])( + 'keeps the router plugin scoped to %s router-only projects', + (frameworkId, startPackage) => { + const framework = { + id: frameworkId, + basePackageJSON: { + dependencies: { + [startPackage]: 'latest', + }, + }, + optionalPackages: { + typescript: {}, + devtools: {}, + examples: {}, + tailwindcss: {}, + 'file-router': { + devDependencies: { + '@tanstack/router-cli': 'latest', + }, + }, + }, + } as unknown as Framework + const options = { + chosenAddOns: [], + addOnOptions: {}, + mode: 'file-router', + typescript: true, + tailwind: false, + projectName: 'test', + packageManager: 'pnpm', + framework, + } as unknown as Options + + const startPackageJSON = createPackageJSON(options) + expect(startPackageJSON.dependencies).toHaveProperty(startPackage, 'latest') + expect(startPackageJSON.devDependencies).not.toHaveProperty( + '@tanstack/router-plugin', + ) + + const routerOnlyOptions = { + ...options, + routerOnly: true, + } + const routerOnlyPackageJSON = createPackageJSON(routerOnlyOptions) + + expect(routerOnlyPackageJSON.dependencies).not.toHaveProperty(startPackage) + expect(routerOnlyPackageJSON.devDependencies).toHaveProperty( + '@tanstack/router-plugin', + 'latest', + ) + expect(createEdgePackageJSON(routerOnlyOptions)).toEqual( + routerOnlyPackageJSON, + ) + }, + ) + it('should create a package.json', () => { const packageJSON = createPackageJSON({ chosenAddOns: [ diff --git a/packages/create/tests/react-template-dependencies.test.ts b/packages/create/tests/react-template-dependencies.test.ts new file mode 100644 index 00000000..73bbbc9c --- /dev/null +++ b/packages/create/tests/react-template-dependencies.test.ts @@ -0,0 +1,68 @@ +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { describe, expect, it } from 'vitest' + +const reactRoot = fileURLToPath( + new URL('../src/frameworks/react', import.meta.url), +) + +interface PackageManifest { + dependencies?: Record + devDependencies?: Record +} + +function readJson(path: string) { + return JSON.parse(readFileSync(path, 'utf8')) as T +} + +function importsLucide(path: string): boolean { + if (!existsSync(path)) return false + + return readdirSync(path).some((entry) => { + const entryPath = join(path, entry) + if (statSync(entryPath).isDirectory()) return importsLucide(entryPath) + if (!/\.[cm]?[jt]sx?$/.test(entry)) return false + + return /from\s+['"]lucide-react['"]/.test(readFileSync(entryPath, 'utf8')) + }) +} + +function lucideConsumers(group: 'add-ons' | 'examples') { + const groupRoot = join(reactRoot, group) + + return readdirSync(groupRoot) + .map((name) => join(groupRoot, name)) + .filter((path) => statSync(path).isDirectory()) + .filter((path) => importsLucide(join(path, 'assets'))) +} + +describe('React template dependency ownership', () => { + it('does not install Lucide in the base or Tailwind profile', () => { + const base = readJson( + join(reactRoot, 'project/base/package.json'), + ) + const profiles = readJson<{ + tailwindcss?: { dependencies?: Record } + }>(join(reactRoot, 'project/packages.json')) + + expect(base.dependencies?.['lucide-react']).toBeUndefined() + expect(profiles.tailwindcss?.dependencies?.['lucide-react']).toBeUndefined() + }) + + it.each([...lucideConsumers('add-ons'), ...lucideConsumers('examples')])( + '%s declares Lucide when its emitted source imports it', + (consumerPath) => { + const manifest = readJson( + join(consumerPath, 'package.json'), + ) + const dependencies = { + ...manifest.dependencies, + ...manifest.devDependencies, + } + + expect(dependencies['lucide-react']).toBeDefined() + }, + ) +})