create-electron-vite
Version:
Scaffolding Your Electron + Vite Project
313 lines (311 loc) • 10.7 kB
JavaScript
import fs from "node:fs";
import path from "node:path";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import prompts from "prompts";
const COLOURS = {
$: (c) => (str) => `\x1B[${c}m` + str + "\x1B[0m",
gary: (str) => COLOURS.$(90)(str),
cyan: (str) => COLOURS.$(36)(str),
yellow: (str) => COLOURS.$(33)(str),
green: (str) => COLOURS.$(32)(str),
red: (str) => COLOURS.$(31)(str)
};
const cwd = process.cwd();
const require2 = createRequire(import.meta.url);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const argTargetDir = process.argv.slice(2).join(" ");
const defaultTargetDir = "electron-vite-project";
const renameFiles = {
_gitignore: ".gitignore"
};
async function init() {
let template;
let targetDir = argTargetDir ?? defaultTargetDir;
const getProjectName = () => targetDir === "." ? path.basename(path.resolve()) : targetDir;
try {
template = await prompts(
[
{
type: () => argTargetDir ? null : "text",
name: "projectName",
message: "Project name:",
initial: defaultTargetDir,
onState: (state) => {
targetDir = (state == null ? void 0 : state.value.trim().replace(/\/+$/g, "")) ?? defaultTargetDir;
}
},
{
type: () => !fs.existsSync(targetDir) || isEmpty(targetDir) ? null : "confirm",
name: "overwrite",
message: () => (targetDir === "." ? "Current directory" : `Target directory "${targetDir}"`) + ` is not empty. Remove existing files and continue?`
},
{
type: (_, { overwrite: overwrite2 }) => {
if (overwrite2 === false) {
throw new Error(COLOURS.red("✖") + " Operation cancelled");
}
return null;
},
name: "overwriteChecker"
},
{
type: () => isValidPackageName(getProjectName()) ? null : "text",
name: "packageName",
message: "Package name:",
initial: () => toValidPackageName(getProjectName()),
validate: (dir) => isValidPackageName(dir) || "Invalid package.json name"
},
{
type: "select",
name: "framework",
message: "Project template:",
choices: [
{
title: "Vue",
value: "vue"
},
{
title: "React",
value: "react"
},
{
title: "Vanilla",
value: "vanilla"
}
]
}
],
{
onCancel: () => {
throw new Error(`${COLOURS.red("✖")} Operation cancelled`);
}
}
);
} catch (cancelled) {
console.log(cancelled.message);
return;
}
const { overwrite, framework, packageName } = template;
const root = path.join(cwd, targetDir);
if (overwrite) {
emptyDir(root);
} else if (!fs.existsSync(root)) {
fs.mkdirSync(root, { recursive: true });
}
console.log(`
Scaffolding project in ${root}...`);
const templateDir = path.resolve(__dirname, "..", `template-${framework}-ts`);
const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent);
const pkgManager = pkgInfo ? pkgInfo.name : "npm";
const write = (file, content) => {
const targetPath = path.join(root, renameFiles[file] ?? file);
if (content) {
fs.writeFileSync(targetPath, content);
} else {
copy(path.join(templateDir, file), targetPath);
}
};
const files = fs.readdirSync(templateDir);
for (const file of files.filter((f) => f !== "package.json")) {
write(file);
}
const pkg = JSON.parse(
fs.readFileSync(path.join(templateDir, "package.json"), "utf-8")
);
pkg.name = packageName || getProjectName();
write("package.json", JSON.stringify(pkg, null, 2) + "\n");
setupElectron(root, framework);
console.log(`
Done. Now run:
`);
const cdProjectName = path.relative(cwd, root);
if (root !== cwd) {
console.log(` cd ${cdProjectName.includes(" ") ? `"${cdProjectName}"` : cdProjectName}`);
}
switch (pkgManager) {
case "yarn":
console.log(" yarn");
console.log(" yarn dev");
break;
default:
console.log(` ${pkgManager} install`);
console.log(` ${pkgManager} run dev`);
break;
}
console.log();
}
function isEmpty(path2) {
const files = fs.readdirSync(path2);
return files.length === 0 || files.length === 1 && files[0] === ".git";
}
function emptyDir(dir) {
if (!fs.existsSync(dir)) {
return;
}
for (const file of fs.readdirSync(dir)) {
if (file === ".git") {
continue;
}
fs.rmSync(path.resolve(dir, file), { recursive: true, force: true });
}
}
function isValidPackageName(projectName) {
return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(projectName);
}
function toValidPackageName(projectName) {
return projectName.trim().toLowerCase().replace(/\s+/g, "-").replace(/^[._]/, "").replace(/[^a-z\d\-~]+/g, "-");
}
function copy(src, dest) {
const stat = fs.statSync(src);
if (stat.isDirectory()) {
copyDir(src, dest);
} else {
fs.copyFileSync(src, dest);
}
}
function copyDir(srcDir, destDir) {
fs.mkdirSync(destDir, { recursive: true });
for (const file of fs.readdirSync(srcDir)) {
const srcFile = path.resolve(srcDir, file);
const destFile = path.resolve(destDir, file);
copy(srcFile, destFile);
}
}
function editFile(file, callback) {
const content = fs.readFileSync(file, "utf-8");
fs.writeFileSync(file, callback(content), "utf-8");
}
function pkgFromUserAgent(userAgent) {
if (!userAgent) return void 0;
const pkgSpec = userAgent.split(" ")[0];
const pkgSpecArr = pkgSpec.split("/");
return {
name: pkgSpecArr[0],
version: pkgSpecArr[1]
};
}
function setupElectron(root, framework) {
const sourceDir = path.resolve(__dirname, "..", "electron");
const electronDir = path.join(root, "electron");
const publicDir = path.join(root, "public");
const pkg = require2("../electron/package.json");
fs.mkdirSync(electronDir, { recursive: true });
for (const name of [
"electron-env.d.ts",
"main.ts",
"preload.ts"
]) {
fs.copyFileSync(path.join(sourceDir, name), path.join(electronDir, name));
}
for (const name of [
"electron-vite.animate.svg",
"electron-vite.svg"
]) {
fs.copyFileSync(path.join(sourceDir, name), path.join(publicDir, name));
}
for (const name of [
"electron-builder.json5"
]) {
fs.copyFileSync(path.join(sourceDir, name), path.join(root, name));
}
editFile(path.join(root, "package.json"), (content) => {
const json = JSON.parse(content);
json.main = "dist-electron/main.js";
json.scripts.build = `${json.scripts.build} && electron-builder`;
json.devDependencies.electron = pkg.devDependencies.electron;
json.devDependencies["electron-builder"] = pkg.devDependencies["electron-builder"];
json.devDependencies["vite-plugin-electron"] = pkg.devDependencies["vite-plugin-electron"];
json.devDependencies["vite-plugin-electron-renderer"] = pkg.devDependencies["vite-plugin-electron-renderer"];
return JSON.stringify(json, null, 2) + "\n";
});
const snippets = (indent = 0) => `
// Use contextBridge
window.ipcRenderer.on('main-process-message', (_event, message) => {
console.log(message)
})
`.trim().split("\n").map((line) => line ? " ".repeat(indent) + line : line).join("\n");
if (framework === "vue") {
editFile(
path.join(root, "src/main.ts"),
(content) => content.replace(`mount('#app')`, `mount('#app').$nextTick(() => {
${snippets(2)}
})`)
);
} else if (framework === "react") {
editFile(path.join(root, "src/main.tsx"), (content) => `${content}
${snippets()}
`);
} else if (framework === "vanilla") {
editFile(path.join(root, "src/main.ts"), (content) => `${content}
${snippets()}
`);
}
const electronPlugin = `electron({
main: {
// Shortcut of \`build.lib.entry\`.
entry: 'electron/main.ts',
},
preload: {
// Shortcut of \`build.rollupOptions.input\`.
// Preload scripts may contain Web assets, so use the \`build.rollupOptions.input\` instead \`build.lib.entry\`.
input: path.join(__dirname, 'electron/preload.ts'),
},
// Ployfill the Electron and Node.js API for Renderer process.
// If you want use Node.js in Renderer process, the \`nodeIntegration\` needs to be enabled in the Main process.
// See 👉 https://github.com/electron-vite/vite-plugin-electron-renderer
renderer: process.env.NODE_ENV === 'test'
// https://github.com/electron-vite/vite-plugin-electron-renderer/issues/78#issuecomment-2053600808
? undefined
: {},
})`;
if (framework === "vue" || framework === "react") {
editFile(
path.join(root, "vite.config.ts"),
(content) => content.split("\n").map((line) => line.includes("import { defineConfig } from 'vite'") ? `${line}
import path from 'node:path'
import electron from 'vite-plugin-electron/simple'` : line).map((line) => line.trimStart().startsWith("plugins") ? ` plugins: [
${framework}(),
${electronPlugin},
],` : line).join("\n")
);
} else {
fs.writeFileSync(
path.join(root, "vite.config.ts"),
`
import { defineConfig } from 'vite'
import path from 'node:path'
import electron from 'vite-plugin-electron/simple'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
${electronPlugin},
],
})
`.trimStart()
);
}
editFile(
path.join(root, "tsconfig.json"),
(content) => content.split("\n").map((line) => line.trimStart().startsWith('"include"') ? line.replace("]", ', "electron"]') : line).join("\n")
);
editFile(
path.join(root, ".gitignore"),
(content) => content.split("\n").map((line) => line === "dist-ssr" ? `${line}
dist-electron
release` : line).join("\n")
);
if (framework === "vue") {
editFile(path.join(root, "src/App.vue"), (content) => content.replace("https://vitejs.dev", "https://electron-vite.github.io").replace("/vite.svg", "/electron-vite.svg"));
} else if (framework === "react") {
editFile(path.join(root, "src/App.tsx"), (content) => content.replace("https://vitejs.dev", "https://electron-vite.github.io").replace("/vite.svg", "/electron-vite.animate.svg"));
} else if (framework === "vanilla") {
editFile(path.join(root, "src/main.ts"), (content) => content.replace("https://vitejs.dev", "https://electron-vite.github.io").replace("/vite.svg", "/electron-vite.svg"));
}
}
init().catch((e) => {
console.error(e);
});
export {
COLOURS
};