Skip to content
Merged
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
12 changes: 12 additions & 0 deletions .changeset/solid-v2-templates.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
---
"create-solid": minor
"@solid-cli/create": minor
---

Solid 2.0 template support

- New "Solid 2.0" top-level project type scaffolding the `solid-v2/*` templates from solidjs/templates (basic, bare, fullstack, fullstack-tanstack, with-\*). Listed first but not preselected while Solid 2.0 core is in beta; existing flags (`-s`, `-v`, `-l`, `--v2`) keep their meaning, plus new `--solid` and `--ssr` flags.
- Optional streaming SSR on templates that support it (currently `basic`): a scaffold-time flip that sets `ssr: true` in `vite.config.ts`, adds the generic production `server.js`, and points the `start` script at it. Defaults to No.
- JavaScript variants of the Solid 2.0 templates via the existing sucrase TS→JS conversion (no `index.html` rewrite; `.ts`/`.tsx` references inside `vite.config` are retargeted, `.d.ts` files dropped, minimal `jsconfig.json`).
- Template lists, subdir paths and per-template flags are now read from a `templates.json` manifest at the templates repo HEAD (2s timeout), with silent fallback to the baked-in lists — so new templates and future repo reorganizations no longer require a CLI release.
- Template tarball downloads can be pinned to a templates-repo ref per CLI release (`TEMPLATES_REF`, overridable via `SOLID_CLI_TEMPLATES_REF`).

@birkskyumbirkskyumAug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ryansolid this pinning part might have caused that we now can't update templates without shipping new cli versions. The tempaltes are on solid v2 rc.0, but the npm create solid template cloning still get beta.34. We typically update the templates more frequent than the cli.

- Template tarball downloads can be pinned to a templates-repo ref per CLI release (TEMPLATES_REF, overridable via SOLID_CLI_TEMPLATES_REF).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think the CLI should just pull the latest from templates as appropriate testing should be done there. I'll release a version without version pinning later so the CLI will always pull the latest templates

@birkskyumbirkskyumAug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i reckon the intention is to make it useful with a fork, and to allow any older CLI version to keep working, which would also enable reorganizing the templates without breaking anything. but with a 24h delay, that flow is likely gonna be too inert.

49 changes: 49 additions & 0 deletions packages/create/src/create-solid-v2.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
import { join } from "node:path";
import { existsSync, writeFileSync } from "node:fs";
import { readFile, writeFile } from "node:fs/promises";
import { handleTSConversion } from "./utils/ts-conversion";
import { GIT_IGNORE, JS_CONFIG_SOLID_V2, SolidV2Template } from "./utils/constants";
import { downloadTemplate } from "./utils/download";
import { applySsrFlip } from "./utils/ssr-flip";

export type CreateSolidV2Args = {
template: SolidV2Template | (string & {});
destination: string;
/** Subdir prefix inside the templates repo (from templates.json), defaults to "solid-v2" */
path?: string;
};

export const createSolidV2 = (args: CreateSolidV2Args, transpile?: boolean, ssr?: boolean) => {
if (transpile) {
return createSolidV2JS(args, ssr);
}
return createSolidV2TS(args, ssr);
};

export const createSolidV2TS = async ({ template, destination, path = "solid-v2" }: CreateSolidV2Args, ssr?: boolean) => {
await downloadTemplate(`${path}/${template}`, destination);
if (ssr) await applySsrFlip(destination);
};

export const createSolidV2JS = async (args: CreateSolidV2Args, ssr?: boolean) => {
// Create typescript project in `<destination>/.project`
// then transpile this to javascript and clean up.
// The SSR flip runs inside the temp dir, before conversion, so the config
// edit happens on the `.ts` source (server.js is already plain JS).
const tempDir = join(args.destination, ".project");
await createSolidV2TS({ ...args, destination: tempDir }, ssr);
await handleTSConversion(tempDir, args.destination, JS_CONFIG_SOLID_V2);
// Solid 2.0 templates have no `index.html` (turnkey entries), but their vite
// config references `.ts`/`.tsx` files by path (setup files, middleware, test
// globs) — retarget those to the transpiled `.js`/`.jsx` output
const viteConfigPath = join(args.destination, "vite.config.js");
if (existsSync(viteConfigPath)) {
const viteConfig = (await readFile(viteConfigPath))
.toString()
.replace(/\.tsx(?=['"])/g, ".jsx")
.replace(/\.ts(?=['"])/g, ".js");
await writeFile(viteConfigPath, viteConfig);
}
// Add .gitignore
writeFileSync(join(args.destination, ".gitignore"), GIT_IGNORE);
};
22 changes: 9 additions & 13 deletions packages/create/src/create-start.ts
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
import { downloadRepo, GithubFetcher } from "@begit/core";
import { join } from "path";
import { writeFileSync } from "fs";
import { handleTSConversion } from "./utils/ts-conversion";
import { GIT_IGNORE, StartTemplate, StartTemplateV2 } from "./utils/constants";
import { downloadTemplate } from "./utils/download";
export type CreateStartArgs = {
template: StartTemplate | StartTemplateV2;
template: StartTemplate | StartTemplateV2 | (string & {});
destination: string;
/** Subdir prefix inside the templates repo (from templates.json), defaults to "solid-start-v2"/"solid-start-v1" */
path?: string;
};

export const createStartTS = ({ template, destination }: CreateStartArgs, v2?: boolean) => {
const subdir = v2 ? `solid-start-v2/${template}` : `solid-start-v1/${template}`;
return downloadRepo(
{
repo: { owner: "solidjs", name: "templates", subdir },
dest: destination,
},
GithubFetcher,
);
export const createStartTS = ({ template, destination, path }: CreateStartArgs, v2?: boolean) => {
const prefix = path ?? (v2 ? "solid-start-v2" : "solid-start-v1");
return downloadTemplate(`${prefix}/${template}`, destination);
};

export const createStartJS = async ({ template, destination }: CreateStartArgs, v2?: boolean) => {
export const createStartJS = async ({ template, destination, path }: CreateStartArgs, v2?: boolean) => {
// Create typescript project in `<destination>/.project`
// then transpile this to javascript and clean up
const tempDir = join(destination, ".project");
await createStartTS({ template, destination: tempDir }, v2);
await createStartTS({ template, destination: tempDir, path }, v2);
await handleTSConversion(tempDir, destination);
// Add .gitignore
writeFileSync(join(destination, ".gitignore"), GIT_IGNORE);
Expand Down
17 changes: 8 additions & 9 deletions packages/create/src/create-vanilla.ts
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
import { downloadRepo, GithubFetcher } from "@begit/core";
import { join } from "node:path";
import { writeFileSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { handleTSConversion } from "./utils/ts-conversion";
import { GIT_IGNORE, VanillaTemplate } from "./utils/constants";
import { downloadTemplate } from "./utils/download";

export type CreateVanillaArgs = {
template: VanillaTemplate;
template: VanillaTemplate | (string & {});
destination: string;
/** Subdir prefix inside the templates repo (from templates.json), defaults to "vanilla" */
path?: string;
};
export const createVanilla = (args: CreateVanillaArgs, transpile?: boolean) => {
if (transpile) {
Expand All@@ -16,18 +18,15 @@ export const createVanilla = (args: CreateVanillaArgs, transpile?: boolean) => {
return createVanillaTS(args);
};

export const createVanillaTS = async ({ template, destination }: CreateVanillaArgs) => {
return await downloadRepo(
{ repo: { owner: "solidjs", name: "templates", subdir: `vanilla/${template}` }, dest: destination },
GithubFetcher,
);
export const createVanillaTS = async ({ template, destination, path = "vanilla" }: CreateVanillaArgs) => {
return await downloadTemplate(`${path}/${template}`, destination);
};

export const createVanillaJS = async ({ template, destination }: CreateVanillaArgs) => {
export const createVanillaJS = async ({ template, destination, path }: CreateVanillaArgs) => {
// Create typescript project in `<destination>/.project`
// then transpile this to javascript and clean up
const tempDir = join(destination, ".project");
await createVanillaTS({ template, destination: tempDir });
await createVanillaTS({ template, destination: tempDir, path });
await handleTSConversion(tempDir, destination);
// Replace `index.tsx` with `index.jsx` in `index.html`
const indexPath = join(destination, "index.html");
Expand Down
84 changes: 66 additions & 18 deletions packages/create/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,13 +3,22 @@ import { createVanilla } from "./create-vanilla";
import * as p from "@clack/prompts";
import { cancelable, spinnerify } from "@solid-cli/utils/ui";
import { createStart } from "./create-start";
import { getTemplatesList, GIT_IGNORE, isValidTemplate, PROJECT_TYPES, ProjectType } from "./utils/constants";
import { createSolidV2 } from "./create-solid-v2";
import { GIT_IGNORE, isValidTemplate, LIBRARY_TEMPLATES, PROJECT_TYPES, ProjectType } from "./utils/constants";
import { fetchTemplatesManifest, groupKeyFor, ManifestTemplate, resolveGroup } from "./utils/manifest";
import { detectPackageManager } from "@solid-cli/utils/package-manager";
import { existsSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { createLibrary } from "./create-library";
import { readFile, writeFile } from "node:fs/promises";
export { createVanilla, createStart, createLibrary };
export { createVanilla, createStart, createLibrary, createSolidV2 };

const PROJECT_TYPE_LABELS: Record<ProjectType, string> = {
solid: "Solid 2.0 (Beta)",
start: "SolidStart (Solid 1.x)",
vanilla: "SolidJS + Vite (Solid 1.x)",
library: "Library",
};

export const createSolid = (version: string) =>
defineCommand({
Expand DownExpand Up@@ -41,6 +50,11 @@ export const createSolid = (version: string) =>
alias: "t",
description: "Template name",
},
"solid": {
type: "boolean",
required: false,
description: "Create a Solid 2.0 project",
},
"solidstart": {
type: "boolean",
required: false,
Expand All@@ -64,6 +78,11 @@ export const createSolid = (version: string) =>
alias: "v",
description: "Create a vanilla (SolidJS + Vite) project",
},
"ssr": {
type: "boolean",
required: false,
description: "Enable server-side rendering (Solid 2.0 templates that support it)",
},
"ts": {
type: "boolean",
required: false,
Expand All@@ -81,9 +100,11 @@ export const createSolid = (version: string) =>
templatePositional,
"project-name": projectNameOptional,
"template": templateOptional,
solid,
solidstart,
library,
vanilla,
ssr,
ts,
js,
v2,
Expand All@@ -92,25 +113,30 @@ export const createSolid = (version: string) =>
// Show prompts for any unknown arguments
let projectName = projectNamePositional ?? projectNameOptional;
let template = templatePositional ?? templateOptional;
let projectType: ProjectType | undefined = solidstart
? "start"
: vanilla
? "vanilla"
: library
? "library"
: undefined;
let projectType: ProjectType | undefined = solid
? "solid"
: solidstart
? "start"
: vanilla
? "vanilla"
: library
? "library"
: undefined;
// False if user has selected ts, true if they have selected js, and undefined if they've done neither
let useJS = ts ? !ts : js ? js : undefined;
// Live template lists/paths from the templates repo; undefined falls back to the baked-in lists
const manifest = await fetchTemplatesManifest();
projectName ??= await cancelable(
p.text({ message: "Project Name", placeholder: "solid-project", defaultValue: "solid-project" }),
);
projectType ??= await cancelable(
p.select({
message: "What type of project would you like to create?",
// Solid 2.0 is listed first but not preselected while core is in beta
initialValue: "start",
options: PROJECT_TYPES.map((t) => ({
value: t,
label: t === "start" ? "SolidStart" : t === "vanilla" ? "SolidJS + Vite" : "Library",
label: PROJECT_TYPE_LABELS[t],
})),
}),
);
Expand All@@ -137,38 +163,60 @@ export const createSolid = (version: string) =>
useJS ??= projectType === "library" ? false : !(await cancelable(p.confirm({ message: "Use Typescript?" })));

if (!projectType) return;
const template_opts = getTemplatesList(projectType, isV2);
const group = projectType === "library" ? undefined : resolveGroup(manifest, groupKeyFor(projectType, isV2));
const template_opts: ManifestTemplate[] = group
? group.templates
: LIBRARY_TEMPLATES.map((name) => ({ name }));
template ??= await cancelable(
p.select({
message: "Which template would you like to use?",
initialValue: "ts",
initialValue: (template_opts.find((t) => t.default) ?? template_opts[0])?.name,
options: template_opts
.filter((s) => (useJS ? s : !s.startsWith("js")))
.map((s: string) => ({ label: s, value: s })),
.filter((t) => (useJS ? t : !t.name.startsWith("js")))
.map((t) => ({ label: t.name, value: t.name })),
}),
);

if (!template) return;
const chosenTemplate = template_opts.find((t) => t.name === template);

// SSR flip: only offered on Solid 2.0 templates that support it (e.g. "basic")
let enableSSR = false;
if (projectType === "solid" && chosenTemplate?.ssrToggle) {
enableSSR =
ssr ??
(await cancelable(
p.confirm({ message: "Enable server-side rendering (streaming SSR)?", initialValue: false }),
));
} else if (ssr) {
p.log.warn(`--ssr is not supported for this template 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 === "start" && isValidTemplate("start", template, isV2)) {
if (projectType === "solid" && chosenTemplate) {
await spinnerify({
startText: "Creating project",
finishText: "Project created 🎉",
fn: () => createSolidV2({ template, destination: projectName, path: group?.path }, transpileToJS, enableSSR),
});
} else if (projectType === "start" && chosenTemplate) {
await spinnerify({
startText: "Creating project",
finishText: "Project created 🎉",
fn: () => createStart({ template, destination: projectName }, transpileToJS, isV2),
fn: () => createStart({ template, destination: projectName, path: group?.path }, transpileToJS, isV2),
});
} else if (projectType === "library" && isValidTemplate("library", template)) {
await spinnerify({
startText: "Creating project",
finishText: "Project created 🎉",
fn: () => createLibrary({ destination: projectName }),
});
} else if (projectType === "vanilla" && isValidTemplate(projectType, template)) {
} else if (projectType === "vanilla" && chosenTemplate) {
await spinnerify({
startText: "Creating project",
finishText: "Project created 🎉",
fn: () => createVanilla({ template, destination: projectName }, transpileToJS),
fn: () => createVanilla({ template, destination: projectName, path: group?.path }, transpileToJS),
});
} else {
p.log.error(`Template ${template} is not valid for project type ${projectType}`);
Expand Down
Loading
Loading