Skip to content

Commit e3f5cf3

Browse files
feat(create): respect the manifest tsOnly flag for TypeScript-only templates
Templates flagged tsOnly in templates.json (solid-v2/with-tsrx, flagged in templates e5ccf24) must never go through the JS conversion: sucrase passes .tsrx files through verbatim, but the conversion deletes tsconfig.json, which carries the template's TSRX editor wiring (@tsrx/typescript-plugin entry + tsrx compiler selector) and backs its tsrx-tsc typecheck script. When the selected template is tsOnly the "Use Typescript?" prompt is skipped entirely; an explicit --js flag (or a JS answer followed by picking a tsOnly template from the list) prints a short notice and continues as TypeScript. Non-tsOnly templates are unaffected. The baked fallback mirrors the manifest: with-tsrx added to SOLID_V2_TEMPLATES with tsOnly in BAKED_GROUPS. Suite: 54 tests / 10 files (6 new). Scaffolded with-tsrx against live templates main: tsconfig.json, .vscode/ and the typecheck script survive, tsrx-tsc passes, dev boots. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9d9db75 commit e3f5cf3

5 files changed

Lines changed: 148 additions & 6 deletions

File tree

‎packages/create/src/index.ts‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,14 +160,23 @@ export const createSolid = (version: string) =>
160160
}
161161
const isV2 = useV2 === "v2";
162162

163-
// Don't offer javascript if `projectType` is library
164-
useJS ??= projectType === "library" ? false : !(await cancelable(p.confirm({ message: "Use Typescript?" })));
165-
166-
if (!projectType) return;
167163
const group = projectType === "library" ? undefined : resolveGroup(manifest, groupKeyFor(projectType, isV2));
168164
const template_opts: ManifestTemplate[] = group
169165
? group.templates
170166
: LIBRARY_TEMPLATES.map((name) => ({ name }));
167+
168+
// TypeScript-only templates (manifest `tsOnly`, e.g. with-tsrx): the JS conversion
169+
// would delete files the template depends on (tsconfig.json carries its editor and
170+
// compiler wiring), so never offer or perform it
171+
const tsOnlyNotice = (name: string) =>
172+
p.log.info(`The ${name} template is TypeScript-only — continuing with TypeScript`);
173+
if (template && template_opts.find((t) => t.name === template)?.tsOnly) {
174+
if (useJS) tsOnlyNotice(template);
175+
useJS = false;
176+
}
177+
178+
// Don't offer javascript if `projectType` is library
179+
useJS ??= projectType === "library" ? false : !(await cancelable(p.confirm({ message: "Use Typescript?" })));
171180
const availableTemplates = template_opts.filter((t) => (useJS ? t : !t.name.startsWith("js")));
172181
// clack's autocomplete always focuses options[0] when the search box is empty (it only
173182
// honors `initialValue` for multi-select), so the manifest's `default`-flagged template
@@ -192,6 +201,13 @@ export const createSolid = (version: string) =>
192201
if (!template) return;
193202
const chosenTemplate = template_opts.find((t) => t.name === template);
194203

204+
// A tsOnly template picked from the interactive list after the user chose
205+
// JavaScript: tell them and continue as TypeScript
206+
if (chosenTemplate?.tsOnly && useJS) {
207+
tsOnlyNotice(template);
208+
useJS = false;
209+
}
210+
195211
// SSR flip: only offered on Solid 2.0 templates that support it (e.g. "basic")
196212
let enableSSR = false;
197213
if (projectType === "solid" && chosenTemplate?.ssrToggle) {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ export const SOLID_V2_TEMPLATES = [
124124
"with-sass",
125125
"with-tailwindcss",
126126
"with-tanstack-router",
127+
"with-tsrx",
127128
"with-unocss",
128129
"with-vitest-browser-mode",
129130
] as const satisfies string[];

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ export type ManifestTemplate = {
1818
default?: boolean;
1919
/** Offer the "Enable server-side rendering?" prompt for this template */
2020
ssrToggle?: boolean;
21+
/**
22+
* Template only works as TypeScript (e.g. with-tsrx, whose tsconfig.json carries
23+
* its editor/compiler wiring) — never offer or perform the JS conversion
24+
*/
25+
tsOnly?: boolean;
2126
};
2227

2328
export type ManifestGroup = {
@@ -39,19 +44,20 @@ export type ManifestGroupKey = "solid" | "start-v2" | "start-v1" | "vanilla";
3944
export const MANIFEST_URL = "https://raw.githubusercontent.com/solidjs/templates/HEAD/templates.json";
4045
const MANIFEST_TIMEOUT_MS = 2000;
4146

42-
const asTemplates = (names: readonly string[], defaultName?: string, ssrToggle?: string): ManifestTemplate[] =>
47+
const asTemplates = (names: readonly string[], defaultName?: string, ssrToggle?: string, tsOnly?: string): ManifestTemplate[] =>
4348
names.map((name) => ({
4449
name,
4550
...(name === defaultName ? { default: true } : {}),
4651
...(name === ssrToggle ? { ssrToggle: true } : {}),
52+
...(name === tsOnly ? { tsOnly: true } : {}),
4753
}));
4854

4955
/** Baked-in fallback, used whenever the manifest can't be fetched or parsed */
5056
export const BAKED_GROUPS: Record<ManifestGroupKey, ManifestGroup> = {
5157
"solid": {
5258
label: "Solid 2.0",
5359
path: "solid-v2",
54-
templates: asTemplates(SOLID_V2_TEMPLATES, "basic", "basic"),
60+
templates: asTemplates(SOLID_V2_TEMPLATES, "basic", "basic", "with-tsrx"),
5561
},
5662
"start-v2": {
5763
label: "SolidStart 2",

‎packages/create/tests/manifest.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ it("drops malformed groups and template entries but keeps the rest", () => {
4040
expect(manifest!.groups["solid"].templates).toEqual([{ name: "basic" }]);
4141
});
4242

43+
it("preserves per-template flags like tsOnly", () => {
44+
const manifest = parseManifest({
45+
version: 1,
46+
groups: { solid: { path: "solid-v2", templates: [{ name: "with-tsrx", tsOnly: true }] } },
47+
});
48+
expect(manifest!.groups["solid"].templates).toEqual([{ name: "with-tsrx", tsOnly: true }]);
49+
});
50+
4351
it("resolves groups from the manifest, falling back to baked-in lists", () => {
4452
const manifest = parseManifest(validManifest);
4553
expect(resolveGroup(manifest, "solid").templates.map((t) => t.name)).toEqual(["basic", "bare"]);
@@ -59,12 +67,15 @@ it("resolves groups from the manifest, falling back to baked-in lists", () => {
5967
"with-sass",
6068
"with-tailwindcss",
6169
"with-tanstack-router",
70+
"with-tsrx",
6271
"with-unocss",
6372
"with-vitest-browser-mode",
6473
]);
6574
const basic = baked.templates.find((t) => t.name === "basic")!;
6675
expect(basic.default).toBe(true);
6776
expect(basic.ssrToggle).toBe(true);
77+
// with-tsrx must never be offered as JavaScript (mirrors the manifest's tsOnly flag)
78+
expect(baked.templates.find((t) => t.name === "with-tsrx")!.tsOnly).toBe(true);
6879
expect(resolveGroup(undefined, "start-v2").path).toBe("solid-start-v2");
6980
expect(resolveGroup(undefined, "start-v1").path).toBe("solid-start-v1");
7081
});
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { runCommand } from "citty";
2+
import { afterEach, beforeEach, expect, it, vi } from "vitest";
3+
import { mkdirSync, rmSync } from "node:fs";
4+
import { join } from "node:path";
5+
import type { CreateSolidV2Args } from "../src/create-solid-v2";
6+
7+
const { autocomplete, confirm, logInfo, createSolidV2 } = vi.hoisted(() => ({
8+
autocomplete: vi.fn(),
9+
confirm: vi.fn(),
10+
logInfo: vi.fn(),
11+
createSolidV2: vi.fn(),
12+
}));
13+
14+
vi.mock("@clack/prompts", async (importOriginal) => {
15+
const original = await importOriginal<typeof import("@clack/prompts")>();
16+
return {
17+
...original,
18+
autocomplete,
19+
confirm,
20+
log: { ...original.log, info: logInfo },
21+
};
22+
});
23+
24+
// The scaffold itself (network download + conversion) is covered by template.test.ts;
25+
// here only the prompt flow and the transpile flag passed down are under test.
26+
vi.mock("../src/create-solid-v2", async (importOriginal) => ({
27+
...(await importOriginal<typeof import("../src/create-solid-v2")>()),
28+
createSolidV2,
29+
}));
30+
31+
import { createSolid } from "../src";
32+
33+
const destinations: string[] = [];
34+
const scratch = (name: string) => {
35+
const destination = join("test", name);
36+
rmSync(destination, { recursive: true, force: true });
37+
destinations.push(destination);
38+
return destination;
39+
};
40+
41+
beforeEach(() => {
42+
// Unroutable manifest URL: falls back to the baked-in lists, so these tests
43+
// also assert the baked with-tsrx entry carries the tsOnly flag
44+
process.env.SOLID_CLI_TEMPLATES_MANIFEST_URL = "http://127.0.0.1:1/templates.json";
45+
// The command writes .gitignore into the destination after scaffolding
46+
createSolidV2.mockImplementation(async ({ destination }: CreateSolidV2Args) =>
47+
mkdirSync(destination, { recursive: true }),
48+
);
49+
});
50+
51+
afterEach(() => {
52+
delete process.env.SOLID_CLI_TEMPLATES_MANIFEST_URL;
53+
vi.clearAllMocks();
54+
for (const destination of destinations.splice(0)) rmSync(destination, { recursive: true, force: true });
55+
});
56+
57+
const transpileArg = () => createSolidV2.mock.calls[0][1];
58+
const noticeCalls = () => logInfo.mock.calls.filter(([message]) => /TypeScript-only/.test(String(message)));
59+
60+
it("skips the TypeScript prompt when a tsOnly template is passed as an argument", async () => {
61+
const destination = scratch("tsonly-arg");
62+
63+
await runCommand(createSolid("test"), { rawArgs: [destination, "--solid", "-t", "with-tsrx"] });
64+
65+
expect(confirm).not.toHaveBeenCalled();
66+
expect(createSolidV2).toHaveBeenCalledOnce();
67+
expect(transpileArg()).toBeFalsy();
68+
});
69+
70+
it("prints a notice and scaffolds TypeScript when --js is passed for a tsOnly template", async () => {
71+
const destination = scratch("tsonly-js-flag");
72+
73+
await runCommand(createSolid("test"), { rawArgs: [destination, "--solid", "-t", "with-tsrx", "--js"] });
74+
75+
expect(confirm).not.toHaveBeenCalled();
76+
expect(noticeCalls()).toHaveLength(1);
77+
expect(noticeCalls()[0][0]).toContain("with-tsrx");
78+
expect(transpileArg()).toBeFalsy();
79+
});
80+
81+
it("forces TypeScript when a tsOnly template is picked interactively after choosing JavaScript", async () => {
82+
const destination = scratch("tsonly-interactive");
83+
autocomplete.mockResolvedValueOnce("with-tsrx");
84+
85+
await runCommand(createSolid("test"), { rawArgs: [destination, "--solid", "--js"] });
86+
87+
expect(noticeCalls()).toHaveLength(1);
88+
expect(transpileArg()).toBeFalsy();
89+
});
90+
91+
it("leaves non-tsOnly templates unaffected: --js still converts and no notice is printed", async () => {
92+
const destination = scratch("tsonly-unaffected-js");
93+
94+
await runCommand(createSolid("test"), { rawArgs: [destination, "--solid", "-t", "bare", "--js"] });
95+
96+
expect(noticeCalls()).toHaveLength(0);
97+
expect(transpileArg()).toBe(true);
98+
});
99+
100+
it("leaves non-tsOnly templates unaffected: the TypeScript prompt is still offered", async () => {
101+
const destination = scratch("tsonly-unaffected-prompt");
102+
confirm.mockResolvedValueOnce(true); // "Use Typescript?" -> yes
103+
104+
await runCommand(createSolid("test"), { rawArgs: [destination, "--solid", "-t", "bare"] });
105+
106+
expect(confirm).toHaveBeenCalledWith({ message: "Use Typescript?" });
107+
expect(transpileArg()).toBeFalsy();
108+
});

0 commit comments

Comments
 (0)