Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion packages/create/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand All @@ -106,6 +120,7 @@ export const createSolid = (version: string) =>
library,
vanilla,
ssr,
devtools,
ts,
js,
v2,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions packages/create/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
21 changes: 21 additions & 0 deletions packages/create/src/utils/dev-deps.ts
Original file line number Diff line number Diff line change
@@ -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" : ""));
};
88 changes: 88 additions & 0 deletions packages/create/tests/devtools.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof import("@clack/prompts")>()),
confirm,
}));

vi.mock("../src/create-solid-v2", async (importOriginal) => ({
...(await importOriginal<typeof import("../src/create-solid-v2")>()),
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"]);
});
Loading