Skip to content

Commit 010cac9

Browse files
brenelzclaude
andauthored
feat(create): offer @solidjs/start-devtools during scaffolding (#89)
Adds a wizard step for Solid 2.0 projects that asks whether to add @solidjs/start-devtools (the development toolbar auto-mounted by @solidjs/vite-plugin). On yes, the package is added to the scaffolded project's devDependencies (^1.0.0-next.4), alphabetized and preserving the template's package.json formatting, so the printed install command picks it up. A --devtools flag answers the prompt non-interactively, mirroring --ssr; on non-Solid-2.0 project types the flag warns and is ignored (the package peer-deps on solid-js ^2.0.0-rc.0). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent e3f5cf3 commit 010cac9

4 files changed

Lines changed: 151 additions & 1 deletion

File tree

‎packages/create/src/index.ts‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,18 @@ import * as p from "@clack/prompts";
44
import { cancelable, spinnerify } from "@solid-cli/utils/ui";
55
import { createStart } from "./create-start";
66
import { createSolidV2 } from "./create-solid-v2";
7-
import { GIT_IGNORE, isValidTemplate, LIBRARY_TEMPLATES, PROJECT_TYPES, ProjectType } from "./utils/constants";
7+
import {
8+
GIT_IGNORE,
9+
isValidTemplate,
10+
LIBRARY_TEMPLATES,
11+
PROJECT_TYPES,
12+
ProjectType,
13+
START_DEVTOOLS_PACKAGE,
14+
START_DEVTOOLS_VERSION,
15+
} from "./utils/constants";
816
import { fetchTemplatesManifest, groupKeyFor, ManifestTemplate, resolveGroup } from "./utils/manifest";
917
import { fuzzyScore, rankedOptionsFn } from "./utils/fuzzy";
18+
import { addDevDependency } from "./utils/dev-deps";
1019
import { detectPackageManager } from "@solid-cli/utils/package-manager";
1120
import { existsSync, writeFileSync } from "node:fs";
1221
import { join } from "node:path";
@@ -84,6 +93,11 @@ export const createSolid = (version: string) =>
8493
required: false,
8594
description: "Enable server-side rendering (Solid 2.0 templates that support it)",
8695
},
96+
"devtools": {
97+
type: "boolean",
98+
required: false,
99+
description: `Add ${START_DEVTOOLS_PACKAGE} (Solid 2.0 projects only)`,
100+
},
87101
"ts": {
88102
type: "boolean",
89103
required: false,
@@ -106,6 +120,7 @@ export const createSolid = (version: string) =>
106120
library,
107121
vanilla,
108122
ssr,
123+
devtools,
109124
ts,
110125
js,
111126
v2,
@@ -220,6 +235,18 @@ export const createSolid = (version: string) =>
220235
p.log.warn(`--ssr is not supported for this template and will be ignored`);
221236
}
222237

238+
// Dev toolbar: only offered on Solid 2.0 projects (it peer-deps on solid-js 2.0)
239+
let addDevtools = false;
240+
if (projectType === "solid") {
241+
addDevtools =
242+
devtools ??
243+
(await cancelable(
244+
p.confirm({ message: `Add ${START_DEVTOOLS_PACKAGE} (development toolbar)?`, initialValue: false }),
245+
));
246+
} else if (devtools) {
247+
p.log.warn(`--devtools is only supported for Solid 2.0 projects and will be ignored`);
248+
}
249+
223250
// Need to transpile if the user wants Jabascript, but their selected template isn't Javascript
224251
const transpileToJS = useJS && !template.startsWith("js");
225252
if (projectType === "solid" && chosenTemplate) {
@@ -250,6 +277,11 @@ export const createSolid = (version: string) =>
250277
p.log.error(`Template ${template} is not valid for project type ${projectType}`);
251278
process.exit(0);
252279
}
280+
// The Solid vite plugin mounts the toolbar automatically in development,
281+
// so adding the dependency is the whole setup
282+
if (addDevtools) {
283+
await addDevDependency(projectName, START_DEVTOOLS_PACKAGE, START_DEVTOOLS_VERSION);
284+
}
253285
// Add .gitignore
254286
writeFileSync(join(projectName, ".gitignore"), GIT_IGNORE);
255287
// Add "Created with Solid CLI" text to bottom of README

‎packages/create/src/utils/constants.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,15 @@ export const SOLID_V2_TEMPLATES = [
130130
] as const satisfies string[];
131131
export type SolidV2Template = (typeof SOLID_V2_TEMPLATES)[number];
132132

133+
/**
134+
* Dev toolbar offered by the wizard on Solid 2.0 scaffolds. It peer-deps on
135+
* solid-js ^2.0.0-rc.0 (so 1.x project types don't qualify) and is mounted
136+
* automatically by @solidjs/vite-plugin in development, so adding the
137+
* dependency is the whole setup.
138+
*/
139+
export const START_DEVTOOLS_PACKAGE = "@solidjs/start-devtools";
140+
export const START_DEVTOOLS_VERSION = "^1.0.0-next.4";
141+
133142
/**Supported Library Templates */
134143
export const LIBRARY_TEMPLATES = ["solid-lib-starter"] as const satisfies string[];
135144
export type LibraryTemplate = (typeof LIBRARY_TEMPLATES)[number];
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { existsSync } from "node:fs";
2+
import { readFile, writeFile } from "node:fs/promises";
3+
import { join } from "node:path";
4+
5+
/**
6+
* Add a package to the scaffolded project's `devDependencies`, keeping the
7+
* block alphabetized like the templates ship it. Indentation and the
8+
* trailing newline are detected from the file so the rewrite doesn't churn
9+
* the template's formatting. No-op when the project has no package.json.
10+
*/
11+
export const addDevDependency = async (projectDir: string, name: string, version: string) => {
12+
const pkgPath = join(projectDir, "package.json");
13+
if (!existsSync(pkgPath)) return;
14+
const raw = (await readFile(pkgPath)).toString();
15+
const pkg = JSON.parse(raw);
16+
pkg.devDependencies = Object.fromEntries(
17+
Object.entries({ ...pkg.devDependencies, [name]: version }).sort(([a], [b]) => a.localeCompare(b)),
18+
);
19+
const indent = raw.match(/^([ \t]+)"/m)?.[1] ?? " ";
20+
await writeFile(pkgPath, JSON.stringify(pkg, null, indent) + (raw.endsWith("\n") ? "\n" : ""));
21+
};
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { runCommand } from "citty";
2+
import { afterEach, beforeEach, expect, it, vi } from "vitest";
3+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4+
import { tmpdir } from "node:os";
5+
import { join } from "node:path";
6+
import { addDevDependency } from "../src/utils/dev-deps";
7+
import { START_DEVTOOLS_PACKAGE, START_DEVTOOLS_VERSION } from "../src/utils/constants";
8+
9+
const { confirm, createSolidV2 } = vi.hoisted(() => ({
10+
confirm: vi.fn(),
11+
createSolidV2: vi.fn(),
12+
}));
13+
14+
vi.mock("@clack/prompts", async (importOriginal) => ({
15+
...(await importOriginal<typeof import("@clack/prompts")>()),
16+
confirm,
17+
}));
18+
19+
vi.mock("../src/create-solid-v2", async (importOriginal) => ({
20+
...(await importOriginal<typeof import("../src/create-solid-v2")>()),
21+
createSolidV2,
22+
}));
23+
24+
import { createSolid } from "../src";
25+
26+
const TEMPLATE_PKG = {
27+
name: "example-basic",
28+
devDependencies: { "typescript": "^5.9.2", "vite": "^8.1.5" },
29+
dependencies: { "solid-js": "^2.0.0-rc.0" },
30+
};
31+
32+
let projectDir: string;
33+
beforeEach(() => {
34+
// Unroutable address: fetchTemplatesManifest fails fast and falls back to the
35+
// baked-in template lists, same trick used in manifest.test.ts / cli.test.ts.
36+
process.env.SOLID_CLI_TEMPLATES_MANIFEST_URL = "http://127.0.0.1:1/templates.json";
37+
projectDir = join(mkdtempSync(join(tmpdir(), "solid-cli-devtools-")), "app");
38+
// Stand in for the template download: just materialize a package.json
39+
createSolidV2.mockImplementation(async ({ destination }: { destination: string }) => {
40+
mkdirSync(destination, { recursive: true });
41+
writeFileSync(join(destination, "package.json"), JSON.stringify(TEMPLATE_PKG, null, 2) + "\n");
42+
});
43+
});
44+
afterEach(() => {
45+
delete process.env.SOLID_CLI_TEMPLATES_MANIFEST_URL;
46+
rmSync(join(projectDir, ".."), { recursive: true, force: true });
47+
vi.clearAllMocks();
48+
});
49+
50+
const readPkg = () => JSON.parse(readFileSync(join(projectDir, "package.json")).toString());
51+
52+
it("prompts for start-devtools on Solid 2.0 projects and adds it on yes", async () => {
53+
confirm.mockResolvedValueOnce(true);
54+
55+
// --ssr answers the SSR toggle up front, so the only confirm left is devtools
56+
await runCommand(createSolid("test"), { rawArgs: [projectDir, "basic", "--solid", "--ts", "--ssr"] });
57+
58+
expect(confirm).toHaveBeenCalledTimes(1);
59+
expect(confirm.mock.calls[0][0].message).toContain(START_DEVTOOLS_PACKAGE);
60+
expect(readPkg().devDependencies[START_DEVTOOLS_PACKAGE]).toBe(START_DEVTOOLS_VERSION);
61+
});
62+
63+
it("leaves the project untouched when the devtools prompt is declined", async () => {
64+
confirm.mockResolvedValueOnce(false);
65+
66+
await runCommand(createSolid("test"), { rawArgs: [projectDir, "basic", "--solid", "--ts", "--ssr"] });
67+
68+
expect(readPkg().devDependencies[START_DEVTOOLS_PACKAGE]).toBeUndefined();
69+
});
70+
71+
it("skips the prompt when --devtools is passed", async () => {
72+
await runCommand(createSolid("test"), { rawArgs: [projectDir, "basic", "--solid", "--ts", "--ssr", "--devtools"] });
73+
74+
expect(confirm).not.toHaveBeenCalled();
75+
expect(readPkg().devDependencies[START_DEVTOOLS_PACKAGE]).toBe(START_DEVTOOLS_VERSION);
76+
});
77+
78+
it("keeps devDependencies alphabetized and preserves the template's formatting", async () => {
79+
mkdirSync(projectDir, { recursive: true });
80+
writeFileSync(join(projectDir, "package.json"), JSON.stringify(TEMPLATE_PKG, null, 2) + "\n");
81+
82+
await addDevDependency(projectDir, START_DEVTOOLS_PACKAGE, START_DEVTOOLS_VERSION);
83+
84+
const raw = readFileSync(join(projectDir, "package.json")).toString();
85+
expect(raw.endsWith("\n")).toBe(true);
86+
expect(raw).toContain(' "devDependencies"');
87+
expect(Object.keys(readPkg().devDependencies)).toEqual([START_DEVTOOLS_PACKAGE, "typescript", "vite"]);
88+
});

0 commit comments

Comments
 (0)