diff --git a/packages/create/src/index.ts b/packages/create/src/index.ts index dadf851..d722fa3 100644 --- a/packages/create/src/index.ts +++ b/packages/create/src/index.ts @@ -4,9 +4,18 @@ import * as p from "@clack/prompts"; import { cancelable, spinnerify } from "@solid-cli/utils/ui"; import { createStart } from "./create-start"; import { createSolidV2 } from "./create-solid-v2"; -import { GIT_IGNORE, isValidTemplate, LIBRARY_TEMPLATES, PROJECT_TYPES, ProjectType } from "./utils/constants"; +import { + GIT_IGNORE, + isValidTemplate, + LIBRARY_TEMPLATES, + PROJECT_TYPES, + ProjectType, + START_DEVTOOLS_PACKAGE, + START_DEVTOOLS_VERSION, +} from "./utils/constants"; import { fetchTemplatesManifest, groupKeyFor, ManifestTemplate, resolveGroup } from "./utils/manifest"; import { fuzzyScore, rankedOptionsFn } from "./utils/fuzzy"; +import { addDevDependency } from "./utils/dev-deps"; import { detectPackageManager } from "@solid-cli/utils/package-manager"; import { existsSync, writeFileSync } from "node:fs"; import { join } from "node:path"; @@ -84,6 +93,11 @@ export const createSolid = (version: string) => required: false, description: "Enable server-side rendering (Solid 2.0 templates that support it)", }, + "devtools": { + type: "boolean", + required: false, + description: `Add ${START_DEVTOOLS_PACKAGE} (Solid 2.0 projects only)`, + }, "ts": { type: "boolean", required: false, @@ -106,6 +120,7 @@ export const createSolid = (version: string) => library, vanilla, ssr, + devtools, ts, js, v2, @@ -204,6 +219,18 @@ export const createSolid = (version: string) => p.log.warn(`--ssr is not supported for this template and will be ignored`); } + // Dev toolbar: only offered on Solid 2.0 projects (it peer-deps on solid-js 2.0) + let addDevtools = false; + if (projectType === "solid") { + addDevtools = + devtools ?? + (await cancelable( + p.confirm({ message: `Add ${START_DEVTOOLS_PACKAGE} (development toolbar)?`, initialValue: false }), + )); + } else if (devtools) { + p.log.warn(`--devtools is only supported for Solid 2.0 projects and will be ignored`); + } + // Need to transpile if the user wants Jabascript, but their selected template isn't Javascript const transpileToJS = useJS && !template.startsWith("js"); if (projectType === "solid" && chosenTemplate) { @@ -234,6 +261,11 @@ export const createSolid = (version: string) => p.log.error(`Template ${template} is not valid for project type ${projectType}`); process.exit(0); } + // The Solid vite plugin mounts the toolbar automatically in development, + // so adding the dependency is the whole setup + if (addDevtools) { + await addDevDependency(projectName, START_DEVTOOLS_PACKAGE, START_DEVTOOLS_VERSION); + } // Add .gitignore writeFileSync(join(projectName, ".gitignore"), GIT_IGNORE); // Add "Created with Solid CLI" text to bottom of README diff --git a/packages/create/src/utils/constants.ts b/packages/create/src/utils/constants.ts index 6a0ecb6..21d70e1 100644 --- a/packages/create/src/utils/constants.ts +++ b/packages/create/src/utils/constants.ts @@ -129,6 +129,15 @@ export const SOLID_V2_TEMPLATES = [ ] as const satisfies string[]; export type SolidV2Template = (typeof SOLID_V2_TEMPLATES)[number]; +/** + * Dev toolbar offered by the wizard on Solid 2.0 scaffolds. It peer-deps on + * solid-js ^2.0.0-rc.0 (so 1.x project types don't qualify) and is mounted + * automatically by @solidjs/vite-plugin in development, so adding the + * dependency is the whole setup. + */ +export const START_DEVTOOLS_PACKAGE = "@solidjs/start-devtools"; +export const START_DEVTOOLS_VERSION = "^1.0.0-next.4"; + /**Supported Library Templates */ export const LIBRARY_TEMPLATES = ["solid-lib-starter"] as const satisfies string[]; export type LibraryTemplate = (typeof LIBRARY_TEMPLATES)[number]; diff --git a/packages/create/src/utils/dev-deps.ts b/packages/create/src/utils/dev-deps.ts new file mode 100644 index 0000000..4f2baf6 --- /dev/null +++ b/packages/create/src/utils/dev-deps.ts @@ -0,0 +1,21 @@ +import { existsSync } from "node:fs"; +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +/** + * Add a package to the scaffolded project's `devDependencies`, keeping the + * block alphabetized like the templates ship it. Indentation and the + * trailing newline are detected from the file so the rewrite doesn't churn + * the template's formatting. No-op when the project has no package.json. + */ +export const addDevDependency = async (projectDir: string, name: string, version: string) => { + const pkgPath = join(projectDir, "package.json"); + if (!existsSync(pkgPath)) return; + const raw = (await readFile(pkgPath)).toString(); + const pkg = JSON.parse(raw); + pkg.devDependencies = Object.fromEntries( + Object.entries({ ...pkg.devDependencies, [name]: version }).sort(([a], [b]) => a.localeCompare(b)), + ); + const indent = raw.match(/^([ \t]+)"/m)?.[1] ?? " "; + await writeFile(pkgPath, JSON.stringify(pkg, null, indent) + (raw.endsWith("\n") ? "\n" : "")); +}; diff --git a/packages/create/tests/devtools.test.ts b/packages/create/tests/devtools.test.ts new file mode 100644 index 0000000..3b82773 --- /dev/null +++ b/packages/create/tests/devtools.test.ts @@ -0,0 +1,88 @@ +import { runCommand } from "citty"; +import { afterEach, beforeEach, expect, it, vi } from "vitest"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { addDevDependency } from "../src/utils/dev-deps"; +import { START_DEVTOOLS_PACKAGE, START_DEVTOOLS_VERSION } from "../src/utils/constants"; + +const { confirm, createSolidV2 } = vi.hoisted(() => ({ + confirm: vi.fn(), + createSolidV2: vi.fn(), +})); + +vi.mock("@clack/prompts", async (importOriginal) => ({ + ...(await importOriginal()), + confirm, +})); + +vi.mock("../src/create-solid-v2", async (importOriginal) => ({ + ...(await importOriginal()), + createSolidV2, +})); + +import { createSolid } from "../src"; + +const TEMPLATE_PKG = { + name: "example-basic", + devDependencies: { "typescript": "^5.9.2", "vite": "^8.1.5" }, + dependencies: { "solid-js": "^2.0.0-rc.0" }, +}; + +let projectDir: string; +beforeEach(() => { + // Unroutable address: fetchTemplatesManifest fails fast and falls back to the + // baked-in template lists, same trick used in manifest.test.ts / cli.test.ts. + process.env.SOLID_CLI_TEMPLATES_MANIFEST_URL = "http://127.0.0.1:1/templates.json"; + projectDir = join(mkdtempSync(join(tmpdir(), "solid-cli-devtools-")), "app"); + // Stand in for the template download: just materialize a package.json + createSolidV2.mockImplementation(async ({ destination }: { destination: string }) => { + mkdirSync(destination, { recursive: true }); + writeFileSync(join(destination, "package.json"), JSON.stringify(TEMPLATE_PKG, null, 2) + "\n"); + }); +}); +afterEach(() => { + delete process.env.SOLID_CLI_TEMPLATES_MANIFEST_URL; + rmSync(join(projectDir, ".."), { recursive: true, force: true }); + vi.clearAllMocks(); +}); + +const readPkg = () => JSON.parse(readFileSync(join(projectDir, "package.json")).toString()); + +it("prompts for start-devtools on Solid 2.0 projects and adds it on yes", async () => { + confirm.mockResolvedValueOnce(true); + + // --ssr answers the SSR toggle up front, so the only confirm left is devtools + await runCommand(createSolid("test"), { rawArgs: [projectDir, "basic", "--solid", "--ts", "--ssr"] }); + + expect(confirm).toHaveBeenCalledTimes(1); + expect(confirm.mock.calls[0][0].message).toContain(START_DEVTOOLS_PACKAGE); + expect(readPkg().devDependencies[START_DEVTOOLS_PACKAGE]).toBe(START_DEVTOOLS_VERSION); +}); + +it("leaves the project untouched when the devtools prompt is declined", async () => { + confirm.mockResolvedValueOnce(false); + + await runCommand(createSolid("test"), { rawArgs: [projectDir, "basic", "--solid", "--ts", "--ssr"] }); + + expect(readPkg().devDependencies[START_DEVTOOLS_PACKAGE]).toBeUndefined(); +}); + +it("skips the prompt when --devtools is passed", async () => { + await runCommand(createSolid("test"), { rawArgs: [projectDir, "basic", "--solid", "--ts", "--ssr", "--devtools"] }); + + expect(confirm).not.toHaveBeenCalled(); + expect(readPkg().devDependencies[START_DEVTOOLS_PACKAGE]).toBe(START_DEVTOOLS_VERSION); +}); + +it("keeps devDependencies alphabetized and preserves the template's formatting", async () => { + mkdirSync(projectDir, { recursive: true }); + writeFileSync(join(projectDir, "package.json"), JSON.stringify(TEMPLATE_PKG, null, 2) + "\n"); + + await addDevDependency(projectDir, START_DEVTOOLS_PACKAGE, START_DEVTOOLS_VERSION); + + const raw = readFileSync(join(projectDir, "package.json")).toString(); + expect(raw.endsWith("\n")).toBe(true); + expect(raw).toContain(' "devDependencies"'); + expect(Object.keys(readPkg().devDependencies)).toEqual([START_DEVTOOLS_PACKAGE, "typescript", "vite"]); +});