Skip to content

Commit 3bc50ee

Browse files
committed
Update create-mastro with template selector
1 parent 792bdd7 commit 3bc50ee

2 files changed

Lines changed: 134 additions & 29 deletions

File tree

create-mastro/index.js

Lines changed: 133 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@ import { createWriteStream } from "node:fs";
1414
import fs from "node:fs/promises";
1515
import { join } from "node:path";
1616
import { stdin, stdout } from "node:process";
17+
import readline from "node:readline";
1718
import { createInterface } from "node:readline/promises";
1819
import { Readable } from "node:stream";
1920

20-
const userAgent = process.env.npm_config_user_agent;
21+
/**
22+
* Constants
23+
*/
2124

25+
const userAgent = process.env.npm_config_user_agent;
2226
const runtime = (() => {
2327
if (typeof Deno === "object") {
2428
return "deno"
@@ -31,6 +35,65 @@ const runtime = (() => {
3135
return "node";
3236
}
3337
})();
38+
const packageManager = (() => {
39+
if (runtime === "deno") return "deno";
40+
switch (userAgent?.split("/")[0]) {
41+
case "pnpm": return "pnpm";
42+
case "yarn": return "yarn";
43+
case "bun": return "bun";
44+
default: return "npm";
45+
}
46+
})();
47+
48+
49+
/**
50+
* Helper Functions
51+
*/
52+
53+
/**
54+
* @template {string} T
55+
* @param {string} question
56+
* @param {T[]} options
57+
* @returns {Promise<T>}
58+
*/
59+
const select = async (question, options) =>
60+
new Promise(resolve => {
61+
let index = 0;
62+
63+
const render = () => {
64+
console.clear();
65+
console.log(question);
66+
options.forEach((opt, i) => {
67+
console.log(i === index ? `● ${opt}` : `\x1b[2m○ ${opt}\x1b[0m`);
68+
});
69+
}
70+
render();
71+
72+
process.stdin.on("keypress", (_, key) => {
73+
switch (key.name) {
74+
case "c": {
75+
if (key.ctrl) {
76+
console.clear();
77+
process.exit();
78+
}
79+
return;
80+
}
81+
case "up": {
82+
index = (index - 1 + options.length) % options.length;
83+
return render();
84+
}
85+
case "down": {
86+
index = (index + 1) % options.length;
87+
return render();
88+
}
89+
case "return": {
90+
console.clear();
91+
process.stdin.removeAllListeners("keypress");
92+
return resolve(options[index]);
93+
}
94+
}
95+
});
96+
});
3497

3598

3699
/**
@@ -66,19 +129,12 @@ const execCmd = (cmd) =>
66129
}))
67130
);
68131

69-
const repoName = `template-basic-${runtime}`;
70-
const repoUrl = `https://github.com/mastrojs/${repoName}/archive/refs/heads/main.zip`;
71-
const zipFilePromise = fetch(repoUrl);
72-
73-
const rl = createInterface({ input: stdin, output: stdout, crlfDelay: Infinity });
74-
const dir = await rl.question("What folder should we create for your new project?\n");
75-
rl.close();
76-
stdin.destroy();
77-
78-
if (dir) {
79-
const outDir = repoName + "-main"; // this cannot be changed and is determined by the zip file
80-
const zipFileName = outDir + ".zip";
81-
const res = await zipFilePromise;
132+
/**
133+
* @param { {fetchZipPromise: Promise<Response>; zipFileName: string } } opts
134+
*/
135+
const unzip = async (opts) => {
136+
const { fetchZipPromise, zipFileName } = opts;
137+
const res = await fetchZipPromise;
82138
if (res.ok && res.body) {
83139
await writeFile(zipFileName, res.body);
84140
}
@@ -92,30 +148,75 @@ if (dir) {
92148
}
93149
await fs.rm(zipFileName, { force: true, recursive: true });
94150

95-
if (unzipSuccess) {
96-
await fs.rename(outDir, dir);
151+
if (!unzipSuccess) {
152+
process.exit(-1);
153+
}
154+
}
97155

98-
const packageManager = (() => {
99-
switch (userAgent?.split("/")[0]) {
100-
case "pnpm": return "pnpm";
101-
case "yarn": return "yarn";
102-
case "bun": return "bun";
103-
default: return "npm";
104-
}
105-
})();
156+
/**
157+
* @param { string } dir
158+
* @param { (dependencies: Record<string, string>) => void } cb
159+
*/
160+
const updateDeps = async (dir, cb) => {
161+
const path = join(dir, runtime === "deno" ? "deno.json" : "package.json");
162+
const json = JSON.parse(await fs.readFile(path, { encoding: "utf8" }));
163+
cb(json[runtime === "deno" ? "imports" : "dependencies"]);
164+
await fs.writeFile(path, JSON.stringify(json, null, 2));
165+
}
166+
167+
168+
/**
169+
* Main function
170+
*/
171+
const main = async () => {
172+
const repoName = `template-basic-${runtime}`;
173+
const fetchZipPromise = fetch(`https://github.com/mastrojs/${repoName}/archive/refs/heads/main.zip`);
174+
175+
const rl = createInterface({ input: stdin, output: stdout, crlfDelay: Infinity });
176+
const dir = await rl.question("What name should we use for your new project folder?\n");
177+
if (dir) {
178+
readline.emitKeypressEvents(process.stdin);
179+
process.stdin.setRawMode(true);
180+
181+
const template = await select("Which template would you like to start with?", ["basic", "blog"]);
182+
const templateFetchZipPromise = template === "basic"
183+
? undefined
184+
: fetch(`https://github.com/mastrojs/mastro/archive/refs/heads/main.zip`);
185+
186+
const zipOutDir = repoName + "-main"; // this cannot be changed and is determined by the zip file
187+
await unzip({ fetchZipPromise, zipFileName: zipOutDir + ".zip" });
188+
await fs.rename(zipOutDir, dir);
106189

107190
if (packageManager === "npm") {
108191
try {
109-
const path = join(dir, "package.json");
110-
const packageJson = JSON.parse(await fs.readFile(path, { encoding: "utf8" }));
111-
packageJson.dependencies["@mastrojs/mastro"] = "npm:@jsr/mastrojs__mastro@^0";
112-
await fs.writeFile(path, JSON.stringify(packageJson, null, 2));
192+
await updateDeps(dir, dependencies => {
193+
dependencies["@mastrojs/mastro"] = "npm:@jsr/mastrojs__mastro@^0";
194+
});
113195
await fs.writeFile(join(dir, ".npmrc"), "@jsr:registry=https://npm.jsr.io");
114196
} catch (e) {
115197
console.error(`Created folder ${dir} but failed to patch it for npm. Please use pnpm instead.`);
116198
}
117199
}
118200

201+
if (templateFetchZipPromise) {
202+
// Update dir with things from @mastrojs/mastro's `examples/blog/` folder.
203+
204+
const templateOutDir = "mastro-main";
205+
await unzip({ fetchZipPromise: templateFetchZipPromise, zipFileName: templateOutDir + ".zip" });
206+
207+
await Promise.all(["components", "data", "routes"].map(async folder => {
208+
await fs.rm(join(dir, folder), { recursive: true, force: true });
209+
return fs.rename(join(templateOutDir, "examples", "blog", folder), join(dir, folder));
210+
}));
211+
await updateDeps(dir, deps => {
212+
deps["@mastrojs/markdown"] = packageManager === "npm"
213+
? "npm:@jsr/mastrojs__markdown@^0"
214+
: "jsr:@mastrojs/markdown@^0";
215+
});
216+
217+
await fs.rm(templateOutDir, { recursive: true });
218+
}
219+
119220
const installInstr = runtime === "deno"
120221
? ""
121222
: `\n\nThen install dependencies with: ${packageManager} install\n`;
@@ -134,5 +235,9 @@ Enter the newly created folder with: %ccd ${dir}${installInstr}
134235
"",
135236
codeStyle,
136237
);
238+
239+
rl.close();
240+
stdin.destroy();
137241
}
138242
}
243+
await main();

create-mastro/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@mastrojs/create-mastro",
3-
"version": "0.0.9",
3+
"version": "0.1.0",
44
"type": "module",
55
"scripts": {
66
"npm-publish": "deno check && npm publish --access public"

0 commit comments

Comments
 (0)