create-vitnode-app 0.0.1-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +78 -0
- package/copy-of-vitnode-app/.vscode/settings.json +10 -0
- package/copy-of-vitnode-app/README.md +40 -0
- package/copy-of-vitnode-app/api/.gitignore_template +32 -0
- package/copy-of-vitnode-app/api/drizzle.config.ts +12 -0
- package/copy-of-vitnode-app/api/src/i18n.ts +38 -0
- package/copy-of-vitnode-app/api/src/index.ts +27 -0
- package/copy-of-vitnode-app/api/src/vitnode.api.config.ts +40 -0
- package/copy-of-vitnode-app/api/tsconfig.json +17 -0
- package/copy-of-vitnode-app/api-bun/src/index.ts +16 -0
- package/copy-of-vitnode-app/api-single-app/drizzle.config.ts +12 -0
- package/copy-of-vitnode-app/api-single-app/src/routes/api/$.ts +32 -0
- package/copy-of-vitnode-app/api-single-app/src/server/api-bridge.ts +22 -0
- package/copy-of-vitnode-app/api-single-app/src/server/vitnode-api.server.ts +29 -0
- package/copy-of-vitnode-app/api-single-app/src/vitnode.api.config.ts +37 -0
- package/copy-of-vitnode-app/docker/docker-compose.yml +30 -0
- package/copy-of-vitnode-app/eslint/.prettierrc.mjs +11 -0
- package/copy-of-vitnode-app/eslint/eslint.config.mjs +19 -0
- package/copy-of-vitnode-app/eslint-react/.prettierrc.mjs +11 -0
- package/copy-of-vitnode-app/eslint-react/eslint.config.mjs +19 -0
- package/copy-of-vitnode-app/monorepo/.gitignore_template +41 -0
- package/copy-of-vitnode-app/monorepo/apps/api/.env.example +12 -0
- package/copy-of-vitnode-app/monorepo/apps/web/.env.example +12 -0
- package/copy-of-vitnode-app/monorepo/turbo.json +53 -0
- package/copy-of-vitnode-app/root/.env.example +22 -0
- package/copy-of-vitnode-app/root/.gitignore_template +41 -0
- package/copy-of-vitnode-app/root/global.d.ts +28 -0
- package/copy-of-vitnode-app/root/src/components/admin-shell.tsx +72 -0
- package/copy-of-vitnode-app/root/src/components/main-header.tsx +33 -0
- package/copy-of-vitnode-app/root/src/i18n.ts +36 -0
- package/copy-of-vitnode-app/root/src/lib/admin-auth.ts +11 -0
- package/copy-of-vitnode-app/root/src/lib/admin-nav.ts +43 -0
- package/copy-of-vitnode-app/root/src/lib/admin-search.ts +37 -0
- package/copy-of-vitnode-app/root/src/lib/auth.ts +66 -0
- package/copy-of-vitnode-app/root/src/lib/content-registry.ts +51 -0
- package/copy-of-vitnode-app/root/src/lib/document-headers.ts +128 -0
- package/copy-of-vitnode-app/root/src/lib/i18n/runtime.ts +75 -0
- package/copy-of-vitnode-app/root/src/lib/i18n/shared.ts +20 -0
- package/copy-of-vitnode-app/root/src/lib/navigation.ts +24 -0
- package/copy-of-vitnode-app/root/src/lib/page-head.ts +19 -0
- package/copy-of-vitnode-app/root/src/locales/app.ts +28 -0
- package/copy-of-vitnode-app/root/src/locales/packages.ts +43 -0
- package/copy-of-vitnode-app/root/src/router.tsx +227 -0
- package/copy-of-vitnode-app/root/src/routes/__root.tsx +212 -0
- package/copy-of-vitnode-app/root/src/routes/_admin/admin.core.index.tsx +70 -0
- package/copy-of-vitnode-app/root/src/routes/_admin.tsx +258 -0
- package/copy-of-vitnode-app/root/src/routes/_main/index.tsx +60 -0
- package/copy-of-vitnode-app/root/src/routes/_main.tsx +64 -0
- package/copy-of-vitnode-app/root/src/server/messages.server.ts +29 -0
- package/copy-of-vitnode-app/root/src/start.ts +88 -0
- package/copy-of-vitnode-app/root/src/styles.css +171 -0
- package/copy-of-vitnode-app/root/src/vitnode.config.ts +58 -0
- package/copy-of-vitnode-app/root/src/vitnode.shell.config.ts +35 -0
- package/copy-of-vitnode-app/root/tsconfig.json +29 -0
- package/copy-of-vitnode-app/root/tsr.config.json +3 -0
- package/copy-of-vitnode-app/root/vite.config.ts +119 -0
- package/copy-of-vitnode-plugin/root/.swcrc +26 -0
- package/copy-of-vitnode-plugin/root/global.d.ts +21 -0
- package/copy-of-vitnode-plugin/root/npmignore.template +17 -0
- package/copy-of-vitnode-plugin/root/tsconfig.build.json +5 -0
- package/copy-of-vitnode-plugin/root/tsconfig.json +25 -0
- package/dist/src/create/create-package-json.js +276 -0
- package/dist/src/create/create-vitnode.js +204 -0
- package/dist/src/create/package-versions.js +49 -0
- package/dist/src/helpers/get-available-package-managers.js +24 -0
- package/dist/src/helpers/get-package-json.js +2 -0
- package/dist/src/helpers/get-package-manager-from-root.js +15 -0
- package/dist/src/helpers/get-vitnode-package-version.js +9 -0
- package/dist/src/helpers/init-vitnode.js +34 -0
- package/dist/src/helpers/install-dependencies.js +71 -0
- package/dist/src/helpers/is-folder-empty.js +48 -0
- package/dist/src/helpers/is-online.js +38 -0
- package/dist/src/helpers/is-writeable.js +11 -0
- package/dist/src/helpers/packages-json.js +1 -0
- package/dist/src/helpers/validate-pkg.js +14 -0
- package/dist/src/helpers/with-If.js +1 -0
- package/dist/src/index.js +68 -0
- package/dist/src/plugin/create/add-plugin-to-workspace.js +74 -0
- package/dist/src/plugin/create/create-package-json.js +54 -0
- package/dist/src/plugin/create/create-plugin-vitnode.js +80 -0
- package/dist/src/plugin/create/route-templates.js +144 -0
- package/dist/src/plugin/index.js +29 -0
- package/dist/src/plugin/questions.js +14 -0
- package/dist/src/plugin/validation.js +76 -0
- package/dist/src/prepare/prepare.js +19 -0
- package/dist/src/questions.js +83 -0
- package/dist/src/validation.js +40 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import consoleColor from "picocolors";
|
|
3
|
+
import { getOnline } from "./is-online.js";
|
|
4
|
+
function printInstallErrorSuggestions(stderr, color) {
|
|
5
|
+
if (stderr.includes("ENOTFOUND") || stderr.includes("network")) {
|
|
6
|
+
console.error(color.yellow("💡 Network error detected. Please check your internet connection."));
|
|
7
|
+
}
|
|
8
|
+
else if (stderr.includes("EACCES") ||
|
|
9
|
+
stderr.includes("permission denied")) {
|
|
10
|
+
console.error(color.yellow("💡 Permission error detected. Try running with elevated privileges or check file permissions."));
|
|
11
|
+
}
|
|
12
|
+
else if (stderr.includes("ENOSPC")) {
|
|
13
|
+
console.error(color.yellow("💡 Disk space error detected. Please free up some disk space."));
|
|
14
|
+
}
|
|
15
|
+
else if (stderr.includes("ERR_PNPM_PEER_DEP_ISSUES")) {
|
|
16
|
+
console.error(color.yellow("💡 Peer dependency issues detected. Consider using --force flag or resolve conflicts manually."));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export const installDependencies = async ({ packageManager: pm, cwd, }) => {
|
|
20
|
+
const packageManager = pm.split("@")[0];
|
|
21
|
+
const isOnline = await getOnline();
|
|
22
|
+
const args = ["install"];
|
|
23
|
+
if (!isOnline) {
|
|
24
|
+
console.log(consoleColor.yellow("You appear to be offline.\nFalling back to the local cache."));
|
|
25
|
+
args.push("--offline");
|
|
26
|
+
}
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
let stdout = "";
|
|
29
|
+
let stderr = "";
|
|
30
|
+
const child = spawn(packageManager, args, {
|
|
31
|
+
stdio: "pipe",
|
|
32
|
+
cwd,
|
|
33
|
+
shell: true,
|
|
34
|
+
env: {
|
|
35
|
+
...process.env,
|
|
36
|
+
ADBLOCK: "1",
|
|
37
|
+
NODE_ENV: "development",
|
|
38
|
+
DISABLE_OPENCOLLECTIVE: "1",
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
child.stdout?.on("data", (data) => {
|
|
42
|
+
const output = data.toString();
|
|
43
|
+
stdout += output;
|
|
44
|
+
});
|
|
45
|
+
child.stderr?.on("data", (data) => {
|
|
46
|
+
const output = data.toString();
|
|
47
|
+
stderr += output;
|
|
48
|
+
});
|
|
49
|
+
child.on("close", code => {
|
|
50
|
+
if (code !== 0) {
|
|
51
|
+
console.error(consoleColor.red(`\n❌ Installation failed with exit code: ${code}`));
|
|
52
|
+
if (stderr) {
|
|
53
|
+
console.error(consoleColor.red("Error output:"));
|
|
54
|
+
console.error(stderr);
|
|
55
|
+
}
|
|
56
|
+
if (stdout) {
|
|
57
|
+
console.log(consoleColor.yellow("Standard output:"));
|
|
58
|
+
console.log(stdout);
|
|
59
|
+
}
|
|
60
|
+
printInstallErrorSuggestions(stderr, consoleColor);
|
|
61
|
+
reject(new Error(`Failed to install dependencies using ${packageManager}. Exit code: ${code}\n${stderr || stdout}`));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
resolve();
|
|
65
|
+
});
|
|
66
|
+
child.on("error", error => {
|
|
67
|
+
console.error(consoleColor.red(`❌ Failed to start ${packageManager}:`), error.message);
|
|
68
|
+
reject(new Error(`Failed to start ${packageManager}: ${error.message}`));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { lstatSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import colors from "picocolors";
|
|
4
|
+
export function isFolderEmpty(root, name) {
|
|
5
|
+
const validFiles = [
|
|
6
|
+
".DS_Store",
|
|
7
|
+
".git",
|
|
8
|
+
".gitattributes",
|
|
9
|
+
".gitignore",
|
|
10
|
+
".gitlab-ci.yml",
|
|
11
|
+
".hg",
|
|
12
|
+
".hgcheck",
|
|
13
|
+
".hgignore",
|
|
14
|
+
".idea",
|
|
15
|
+
".npmignore",
|
|
16
|
+
".travis.yml",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"Thumbs.db",
|
|
19
|
+
"docs",
|
|
20
|
+
"mkdocs.yml",
|
|
21
|
+
"npm-debug.log",
|
|
22
|
+
"yarn-debug.log",
|
|
23
|
+
"yarn-error.log",
|
|
24
|
+
"yarnrc.yml",
|
|
25
|
+
".yarn",
|
|
26
|
+
];
|
|
27
|
+
const conflicts = readdirSync(root).filter(file => !(validFiles.includes(file) || file.endsWith(".iml")));
|
|
28
|
+
if (conflicts.length > 0) {
|
|
29
|
+
console.log(`The directory ${colors.green(name)} contains files that could conflict:\n`);
|
|
30
|
+
for (const file of conflicts) {
|
|
31
|
+
try {
|
|
32
|
+
const stats = lstatSync(join(root, file));
|
|
33
|
+
if (stats.isDirectory()) {
|
|
34
|
+
console.log(`${colors.blue(file)}/`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
console.log(file);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
console.log(file);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
console.log("\nEither try using a new directory name, or remove the files listed above.\n");
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { lookup } from "node:dns/promises";
|
|
3
|
+
import { URL } from "node:url";
|
|
4
|
+
function getProxy() {
|
|
5
|
+
if (process.env.https_proxy) {
|
|
6
|
+
return process.env.https_proxy;
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
const httpsProxy = execSync("npm config get https-proxy").toString().trim();
|
|
10
|
+
return httpsProxy !== "null" ? httpsProxy : undefined;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function getOnline() {
|
|
17
|
+
try {
|
|
18
|
+
await lookup("registry.yarnpkg.com");
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
const proxy = getProxy();
|
|
23
|
+
if (!proxy) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const { hostname } = new URL(proxy);
|
|
27
|
+
if (!hostname) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
await lookup(hostname);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import validateProjectName from "validate-npm-package-name";
|
|
2
|
+
export const validateNpmName = ({ name, }) => {
|
|
3
|
+
const nameValidation = validateProjectName(name);
|
|
4
|
+
if (nameValidation.validForNewPackages) {
|
|
5
|
+
return { valid: true };
|
|
6
|
+
}
|
|
7
|
+
return {
|
|
8
|
+
valid: false,
|
|
9
|
+
problems: [
|
|
10
|
+
...(nameValidation.errors ?? []),
|
|
11
|
+
...(nameValidation.warnings ?? []),
|
|
12
|
+
],
|
|
13
|
+
};
|
|
14
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const withIf = (cond, obj) => (cond ? obj : {});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { input } from "@inquirer/prompts";
|
|
3
|
+
import { Command, Option } from "commander";
|
|
4
|
+
import { basename, resolve } from "node:path";
|
|
5
|
+
import color from "picocolors";
|
|
6
|
+
import { createVitNode } from "./create/create-vitnode.js";
|
|
7
|
+
import { packageJson } from "./helpers/get-package-json.js";
|
|
8
|
+
import { validateNpmName } from "./helpers/validate-pkg.js";
|
|
9
|
+
import { createPlugin } from "./plugin/index.js";
|
|
10
|
+
import { createQuestionsCli } from "./questions.js";
|
|
11
|
+
import { validationProject } from "./validation.js";
|
|
12
|
+
const [major] = process.versions.node.split(".").map(Number);
|
|
13
|
+
if (major < 20) {
|
|
14
|
+
console.error(color.red(`\nError: VitNode requires Node.js version 20 or higher.\nYou are currently using Node.js ${process.versions.node}\n`));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
process.on("uncaughtException", (error) => {
|
|
18
|
+
if (error.name === "ExitPromptError") {
|
|
19
|
+
console.log(color.dim("👋 VitNode setup cancelled - see you next time!"));
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
console.error(color.red("An unexpected error occurred:"));
|
|
23
|
+
console.error(color.red(error.stack ?? error.message));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
|
26
|
+
const init = async () => {
|
|
27
|
+
let projectPath = "";
|
|
28
|
+
const program = new Command()
|
|
29
|
+
.version(packageJson.version ?? "0.1.0")
|
|
30
|
+
.argument("[project-directory]")
|
|
31
|
+
.usage(`${color.green("[project-directory]")} [options]`)
|
|
32
|
+
.action(name => {
|
|
33
|
+
projectPath = name;
|
|
34
|
+
});
|
|
35
|
+
program.addOption(new Option("--package-manager <package-manager>", "Specify the package manager to use").choices(["npm", "pnpm", "bun"]));
|
|
36
|
+
program.option("--eslint", "Initialize with ESLint & Prettier config.");
|
|
37
|
+
program.option("--skip-install", "Skip installing packages after initializing the project.");
|
|
38
|
+
program.addOption(new Option("--mode <mode>", "What type of app do you want to create?").choices(["singleApp", "apiMonorepo", "onlyApi"]));
|
|
39
|
+
program.option("--monorepo", "Create project with monorepo structure.");
|
|
40
|
+
program.option("--docker", "Initialize with Docker support.");
|
|
41
|
+
program.option("--plugin", "Create a plugin.");
|
|
42
|
+
program.parse(process.argv);
|
|
43
|
+
const opts = program.opts();
|
|
44
|
+
if (opts.plugin) {
|
|
45
|
+
await createPlugin({ projectPath, program });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!projectPath) {
|
|
49
|
+
projectPath = await input({
|
|
50
|
+
message: "What is your project named?",
|
|
51
|
+
default: "my-vitnode",
|
|
52
|
+
validate: (name) => {
|
|
53
|
+
const validation = validateNpmName({ name: basename(resolve(name)) });
|
|
54
|
+
if (validation.valid)
|
|
55
|
+
return true;
|
|
56
|
+
return `Invalid project name: ${validation.problems[0]}`;
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const { appName, root } = await validationProject(projectPath);
|
|
61
|
+
const options = await createQuestionsCli(program);
|
|
62
|
+
await createVitNode({
|
|
63
|
+
appName,
|
|
64
|
+
root,
|
|
65
|
+
...options,
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
await init();
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readdir, readFile, writeFile } from "fs/promises";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
const writeJson = async (path, data) => writeFile(path, JSON.stringify(data, null, 2) + "\n");
|
|
4
|
+
const findPackageJsonFiles = async (dir, results = []) => {
|
|
5
|
+
try {
|
|
6
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
7
|
+
for (const entry of entries) {
|
|
8
|
+
const fullPath = join(dir, entry.name);
|
|
9
|
+
if (entry.name === "node_modules" || entry.name === "dist") {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
if (entry.isDirectory()) {
|
|
13
|
+
await findPackageJsonFiles(fullPath, results);
|
|
14
|
+
}
|
|
15
|
+
else if (entry.name === "package.json") {
|
|
16
|
+
results.push(fullPath);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
}
|
|
22
|
+
return results;
|
|
23
|
+
};
|
|
24
|
+
export const addPluginToWorkspace = async ({ packageManager, pluginName, pluginPath, rootPath, }) => {
|
|
25
|
+
const packageJsonFiles = await findPackageJsonFiles(rootPath);
|
|
26
|
+
const pluginParentDir = dirname(pluginPath);
|
|
27
|
+
for (const packageJsonPath of packageJsonFiles) {
|
|
28
|
+
if (packageJsonPath === join(pluginPath, "package.json")) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const packageDir = dirname(dirname(packageJsonPath));
|
|
32
|
+
if (packageDir === pluginParentDir) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const content = await readFile(packageJsonPath, "utf-8");
|
|
37
|
+
const pkg = JSON.parse(content);
|
|
38
|
+
const hasVitnodeCore = pkg.dependencies?.["@vitnode/core"] ??
|
|
39
|
+
pkg.devDependencies?.["@vitnode/core"];
|
|
40
|
+
if (!hasVitnodeCore) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
let workspaceReference;
|
|
44
|
+
switch (packageManager) {
|
|
45
|
+
case "bun":
|
|
46
|
+
workspaceReference = "workspace:*";
|
|
47
|
+
break;
|
|
48
|
+
case "npm":
|
|
49
|
+
workspaceReference = "*";
|
|
50
|
+
break;
|
|
51
|
+
case "pnpm":
|
|
52
|
+
workspaceReference = "workspace:*";
|
|
53
|
+
break;
|
|
54
|
+
case "yarn":
|
|
55
|
+
workspaceReference = "workspace:*";
|
|
56
|
+
break;
|
|
57
|
+
default:
|
|
58
|
+
workspaceReference = "workspace:*";
|
|
59
|
+
}
|
|
60
|
+
pkg.dependencies ??= {};
|
|
61
|
+
pkg.dependencies[pluginName] = workspaceReference;
|
|
62
|
+
pkg.dependencies = Object.keys(pkg.dependencies)
|
|
63
|
+
.sort()
|
|
64
|
+
.reduce((acc, key) => {
|
|
65
|
+
acc[key] = pkg.dependencies?.[key] ?? "";
|
|
66
|
+
return acc;
|
|
67
|
+
}, {});
|
|
68
|
+
await writeJson(packageJsonPath, pkg);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { writeFile } from "fs/promises";
|
|
2
|
+
import { versionsPackageJson } from "../../create/package-versions.js";
|
|
3
|
+
import { getVitnodePackageVersion } from "../../helpers/get-vitnode-package-version.js";
|
|
4
|
+
import { withIf } from "../../helpers/with-If.js";
|
|
5
|
+
import { pluginPackageExports } from "./route-templates.js";
|
|
6
|
+
const writeJson = async (path, data) => writeFile(path, JSON.stringify(data, null, 2));
|
|
7
|
+
export const createPluginPackageJSON = async ({ pluginName, pluginPath, eslint, }) => {
|
|
8
|
+
const vitnodeVersionRange = await getVitnodePackageVersion();
|
|
9
|
+
const pluginPkg = {
|
|
10
|
+
name: pluginName,
|
|
11
|
+
version: "0.1.0",
|
|
12
|
+
private: true,
|
|
13
|
+
type: "module",
|
|
14
|
+
scripts: {
|
|
15
|
+
"build:plugins": "vitnode build",
|
|
16
|
+
dev: "vitnode dev",
|
|
17
|
+
"dev:email": "email dev --dir src/emails",
|
|
18
|
+
...withIf(eslint, {
|
|
19
|
+
lint: "turbo lint",
|
|
20
|
+
"lint:fix": "turbo lint:fix",
|
|
21
|
+
}),
|
|
22
|
+
},
|
|
23
|
+
exports: pluginPackageExports(),
|
|
24
|
+
dependencies: {
|
|
25
|
+
"@hono/zod-openapi": versionsPackageJson.honoZodOpenapi,
|
|
26
|
+
"@vitnode/core": vitnodeVersionRange,
|
|
27
|
+
"drizzle-kit": versionsPackageJson.drizzleKit,
|
|
28
|
+
"drizzle-orm": versionsPackageJson.drizzleOrm,
|
|
29
|
+
hono: versionsPackageJson.hono,
|
|
30
|
+
"lucide-react": versionsPackageJson.lucide,
|
|
31
|
+
react: versionsPackageJson.react,
|
|
32
|
+
"react-dom": versionsPackageJson.reactDom,
|
|
33
|
+
"react-email": versionsPackageJson.reactEmail,
|
|
34
|
+
"react-hook-form": versionsPackageJson.rhf,
|
|
35
|
+
sonner: versionsPackageJson.sonner,
|
|
36
|
+
"use-intl": versionsPackageJson.useIntl,
|
|
37
|
+
zod: versionsPackageJson.zod,
|
|
38
|
+
},
|
|
39
|
+
devDependencies: {
|
|
40
|
+
"@react-email/ui": versionsPackageJson.reactEmailUi,
|
|
41
|
+
"@swc/cli": versionsPackageJson.swcCli,
|
|
42
|
+
"@swc/core": versionsPackageJson.swcCore,
|
|
43
|
+
"@types/react": versionsPackageJson.typesReact,
|
|
44
|
+
"@types/react-dom": versionsPackageJson.typesReactDom,
|
|
45
|
+
"@vitnode/config": vitnodeVersionRange,
|
|
46
|
+
...withIf(eslint, {
|
|
47
|
+
eslint: versionsPackageJson.eslint,
|
|
48
|
+
}),
|
|
49
|
+
"tsc-alias": versionsPackageJson.tscAlias,
|
|
50
|
+
typescript: versionsPackageJson.typescript,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
await writeJson(`${pluginPath}/package.json`, pluginPkg);
|
|
54
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { cp, mkdir, rename, writeFile } from "fs/promises";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
import color from "picocolors";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { getPackageManagerFromRoot } from "../../helpers/get-package-manager-from-root.js";
|
|
8
|
+
import { installDependencies } from "../../helpers/install-dependencies.js";
|
|
9
|
+
import { isFolderEmpty } from "../../helpers/is-folder-empty.js";
|
|
10
|
+
import { addPluginToWorkspace } from "./add-plugin-to-workspace.js";
|
|
11
|
+
import { createPluginPackageJSON } from "./create-package-json.js";
|
|
12
|
+
import { pluginRouteScaffold } from "./route-templates.js";
|
|
13
|
+
const writePluginRouteScaffold = async ({ pluginName, pluginPath, }) => {
|
|
14
|
+
const files = pluginRouteScaffold(pluginName);
|
|
15
|
+
await Promise.all(Object.keys(files).map(async (file) => mkdir(dirname(join(pluginPath, file)), { recursive: true })));
|
|
16
|
+
await Promise.all(Object.entries(files).map(async ([file, contents]) => writeFile(join(pluginPath, file), contents, "utf-8")));
|
|
17
|
+
};
|
|
18
|
+
export const createPluginVitNode = async ({ pluginPath, pluginName, install, eslint, }) => {
|
|
19
|
+
const packageManager = getPackageManagerFromRoot(process.cwd());
|
|
20
|
+
const spinner = ora(`Creating a new VitNode plugin in ${color.green(pluginPath)}. Using ${color.green(packageManager)}...`).start();
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = dirname(__filename);
|
|
23
|
+
const templatePath = join(__dirname, "..", "..", "..", "..", "copy-of-vitnode-plugin", "root");
|
|
24
|
+
if (!existsSync(templatePath)) {
|
|
25
|
+
spinner.fail(`\n${color.red("Error!")} Template path ${color.cyan(templatePath)} does not exist.`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
await mkdir(pluginPath, { recursive: true });
|
|
29
|
+
if (!isFolderEmpty(pluginPath, pluginName)) {
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
spinner.text = "Preparing the plugin structure...";
|
|
33
|
+
await cp(templatePath, pluginPath, { recursive: true });
|
|
34
|
+
const npmIgnoreTemplatePath = join(pluginPath, "npmignore.template");
|
|
35
|
+
const dotNpmIgnorePath = join(pluginPath, ".npmignore");
|
|
36
|
+
if (existsSync(npmIgnoreTemplatePath)) {
|
|
37
|
+
await rename(npmIgnoreTemplatePath, dotNpmIgnorePath);
|
|
38
|
+
}
|
|
39
|
+
spinner.text = "Writing the plugin's first route...";
|
|
40
|
+
await writePluginRouteScaffold({ pluginName, pluginPath });
|
|
41
|
+
spinner.text = "Creating package.json...";
|
|
42
|
+
await createPluginPackageJSON({
|
|
43
|
+
pluginName,
|
|
44
|
+
pluginPath,
|
|
45
|
+
eslint,
|
|
46
|
+
});
|
|
47
|
+
if (eslint) {
|
|
48
|
+
spinner.text = "Setting up ESLint...";
|
|
49
|
+
const templateEslintPath = join(__dirname, "..", "..", "..", "..", "copy-of-vitnode-app", "eslint-react");
|
|
50
|
+
if (!existsSync(templateEslintPath)) {
|
|
51
|
+
spinner.fail(`\n${color.red("Error!")} ESLint template path ${color.cyan(templateEslintPath)} does not exist.`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
await cp(templateEslintPath, pluginPath, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
let rootPath = process.cwd();
|
|
57
|
+
let currentDir = rootPath;
|
|
58
|
+
while (currentDir !== dirname(currentDir)) {
|
|
59
|
+
if (existsSync(join(currentDir, "turbo.json"))) {
|
|
60
|
+
rootPath = currentDir;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
currentDir = dirname(currentDir);
|
|
64
|
+
}
|
|
65
|
+
spinner.text = "Adding plugin to workspace packages...";
|
|
66
|
+
await addPluginToWorkspace({
|
|
67
|
+
packageManager,
|
|
68
|
+
pluginName,
|
|
69
|
+
pluginPath,
|
|
70
|
+
rootPath,
|
|
71
|
+
});
|
|
72
|
+
if (install) {
|
|
73
|
+
spinner.text = "Installing dependencies...";
|
|
74
|
+
await installDependencies({
|
|
75
|
+
packageManager,
|
|
76
|
+
cwd: pluginPath,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
spinner.succeed(`${color.green("Success!")} Created ${color.cyan(pluginName)} at ${color.cyan(pluginPath)}`);
|
|
80
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
export const routeSlugFor = (pluginName) => pluginName.includes("/")
|
|
2
|
+
? pluginName.slice(pluginName.indexOf("/") + 1)
|
|
3
|
+
: pluginName;
|
|
4
|
+
export const pluginRoutesTemplate = (pluginName) => {
|
|
5
|
+
const slug = routeSlugFor(pluginName);
|
|
6
|
+
return `import { definePluginRoutes, lazy, page } from "@vitnode/core/routing";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The routes this plugin contributes to whatever app installs it.
|
|
10
|
+
*
|
|
11
|
+
* Browser-safe data: a path, and the module that renders it. \`lazy\` keeps that
|
|
12
|
+
* \`import()\` a literal the bundler can follow *without running it*, so your page
|
|
13
|
+
* gets a chunk of its own and is fetched when somebody navigates to it - not
|
|
14
|
+
* before. Never import a page into this file: a component named here is in the
|
|
15
|
+
* initial bundle of every page on the site, which is why VitNode refuses one.
|
|
16
|
+
*
|
|
17
|
+
* Your page is never copied into the application either. The app holds one static
|
|
18
|
+
* import of this tree, and nothing else.
|
|
19
|
+
*
|
|
20
|
+
* Add a route by adding a \`page()\`. \`path\` is the public URL, written in
|
|
21
|
+
* VitNode's own spelling: a dynamic segment is \`:id\`, never Next's \`[id]\` and
|
|
22
|
+
* never TanStack's \`$id\`. To nest pages inside a shared frame, wrap them in a
|
|
23
|
+
* \`layout()\` and give each child a path relative to it.
|
|
24
|
+
*/
|
|
25
|
+
export const routes = definePluginRoutes([
|
|
26
|
+
page("/${slug}", {
|
|
27
|
+
component: lazy(() => import("./pages/home-page")),
|
|
28
|
+
}),
|
|
29
|
+
]);
|
|
30
|
+
`;
|
|
31
|
+
};
|
|
32
|
+
export const pluginRouteModuleTemplate = (pluginName) => `import { useTranslations } from "use-intl";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The page \`routes.ts\` declares.
|
|
36
|
+
*
|
|
37
|
+
* Keep it framework-neutral. This module is compiled into the package's own
|
|
38
|
+
* \`dist\` and imported by whichever app installed the plugin, so anything from
|
|
39
|
+
* a router or a host-bound i18n package pins the plugin to one kind of host.
|
|
40
|
+
* \`use-intl\` - which is what VitNode itself renders through - and plain JSX are
|
|
41
|
+
* pinned to neither.
|
|
42
|
+
*
|
|
43
|
+
* No \`<main>\`: the application shell owns the document's one \`main\` landmark,
|
|
44
|
+
* and a page that renders a second gives a screen reader two to choose between.
|
|
45
|
+
* A page owns its container - width, padding, vertical rhythm - and nothing
|
|
46
|
+
* above it.
|
|
47
|
+
*
|
|
48
|
+
* To give this route a loader, page metadata or a breadcrumb, add a \`route\`
|
|
49
|
+
* export beside the default one:
|
|
50
|
+
*
|
|
51
|
+
* import { definePluginRoute } from "@vitnode/core/routing";
|
|
52
|
+
*
|
|
53
|
+
* export const route = definePluginRoute({
|
|
54
|
+
* load: ({ context, params }) => fetchThing(context.locale, params.id),
|
|
55
|
+
* head: ({ loaderData }) => ({ title: loaderData?.title }),
|
|
56
|
+
* });
|
|
57
|
+
*/
|
|
58
|
+
const HomePage = () => {
|
|
59
|
+
const t = useTranslations("${pluginName}");
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="container mx-auto flex max-w-2xl flex-col gap-4 p-4">
|
|
63
|
+
<h1 className="text-2xl font-semibold tracking-tight text-balance">
|
|
64
|
+
{t("home.title")}
|
|
65
|
+
</h1>
|
|
66
|
+
|
|
67
|
+
<p className="text-muted-foreground leading-relaxed text-pretty">
|
|
68
|
+
{t("home.desc")}
|
|
69
|
+
</p>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default HomePage;
|
|
75
|
+
`;
|
|
76
|
+
export const pluginMessagesTemplate = (pluginName) => `${JSON.stringify({
|
|
77
|
+
[pluginName]: {
|
|
78
|
+
home: {
|
|
79
|
+
desc: "This page ships inside the plugin and is served by the app that installed it.",
|
|
80
|
+
title: "Hello from your plugin",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}, null, 2)}\n`;
|
|
84
|
+
export const pluginMessagesBarrelTemplate = () => `import type { LocaleMessagesMap } from "@vitnode/core/lib/i18n/types";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Every language this plugin ships. Add a file next to this one and a line here
|
|
88
|
+
* to add another; apps pick it up with no copy step, because they read this
|
|
89
|
+
* package's own \`dist\` rather than a copy of it.
|
|
90
|
+
*/
|
|
91
|
+
const messages: LocaleMessagesMap = {
|
|
92
|
+
en: async () => await import("./en.json", { with: { type: "json" } }),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export default messages;
|
|
96
|
+
`;
|
|
97
|
+
export const pluginVariableName = (pluginName) => {
|
|
98
|
+
const camel = routeSlugFor(pluginName)
|
|
99
|
+
.split(/[^A-Za-z0-9]+/)
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.map((part, index) => index === 0 ? part : `${part[0].toUpperCase()}${part.slice(1)}`)
|
|
102
|
+
.join("");
|
|
103
|
+
const safe = /^[A-Za-z]/.test(camel) ? camel : `vitnode${camel}`;
|
|
104
|
+
const stem = safe.replace(/plugin$/i, "");
|
|
105
|
+
return `${stem === "" ? safe : stem}Plugin`;
|
|
106
|
+
};
|
|
107
|
+
export const pluginConfigTemplate = (pluginName) => `import { buildPlugin } from "@vitnode/core/lib/plugin";
|
|
108
|
+
|
|
109
|
+
import messages from "./locales";
|
|
110
|
+
import { routes } from "./routes";
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* This plugin, as an application registers it.
|
|
114
|
+
*
|
|
115
|
+
* \`pluginId\` is the package name, and that is not a convention - it is how this
|
|
116
|
+
* plugin's route tree is imported (\`${pluginName}/routes\`) and how its messages
|
|
117
|
+
* are namespaced. The two cannot drift because they are one string.
|
|
118
|
+
*
|
|
119
|
+
* Add this to an app's \`src/vitnode.config.ts\` \`plugins\` array. A plugin that
|
|
120
|
+
* is installed but not listed there contributes nothing - no directory is ever
|
|
121
|
+
* scanned - so this is the only switch.
|
|
122
|
+
*/
|
|
123
|
+
export const ${pluginVariableName(pluginName)} = () =>
|
|
124
|
+
buildPlugin({
|
|
125
|
+
pluginId: "${pluginName}",
|
|
126
|
+
messages,
|
|
127
|
+
routes,
|
|
128
|
+
});
|
|
129
|
+
`;
|
|
130
|
+
export const pluginPackageExports = () => ({
|
|
131
|
+
"./locales/*.json": "./src/locales/*.json",
|
|
132
|
+
"./*": {
|
|
133
|
+
import: "./dist/src/*.js",
|
|
134
|
+
types: "./dist/src/*.d.ts",
|
|
135
|
+
default: "./dist/src/*.js",
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
export const pluginRouteScaffold = (pluginName) => ({
|
|
139
|
+
"src/config.tsx": pluginConfigTemplate(pluginName),
|
|
140
|
+
"src/locales/en.json": pluginMessagesTemplate(pluginName),
|
|
141
|
+
"src/locales/index.ts": pluginMessagesBarrelTemplate(),
|
|
142
|
+
"src/pages/home-page.tsx": pluginRouteModuleTemplate(pluginName),
|
|
143
|
+
"src/routes.ts": pluginRoutesTemplate(pluginName),
|
|
144
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { input } from "@inquirer/prompts";
|
|
2
|
+
import { basename, resolve } from "node:path";
|
|
3
|
+
import { validateNpmName } from "../helpers/validate-pkg.js";
|
|
4
|
+
import { createPluginVitNode } from "./create/create-plugin-vitnode.js";
|
|
5
|
+
import { createPluginQuestionsCli } from "./questions.js";
|
|
6
|
+
import { validationProjectForPlugin } from "./validation.js";
|
|
7
|
+
export const createPlugin = async ({ program, projectPath, }) => {
|
|
8
|
+
let name = projectPath;
|
|
9
|
+
if (!name) {
|
|
10
|
+
name = await input({
|
|
11
|
+
message: "What is your plugin named?",
|
|
12
|
+
default: "my-vitnode-plugin",
|
|
13
|
+
validate: (name) => {
|
|
14
|
+
const validation = validateNpmName({ name: basename(resolve(name)) });
|
|
15
|
+
if (validation.valid)
|
|
16
|
+
return true;
|
|
17
|
+
return `Invalid plugin name: ${validation.problems[0]}`;
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const { pluginName, pluginPath, eslint } = await validationProjectForPlugin(name);
|
|
22
|
+
const options = await createPluginQuestionsCli(program);
|
|
23
|
+
await createPluginVitNode({
|
|
24
|
+
pluginName,
|
|
25
|
+
pluginPath,
|
|
26
|
+
eslint,
|
|
27
|
+
...options,
|
|
28
|
+
});
|
|
29
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { confirm } from "@inquirer/prompts";
|
|
2
|
+
import color from "picocolors";
|
|
3
|
+
export const createPluginQuestionsCli = async (program) => {
|
|
4
|
+
const optionsFromProgram = program.opts();
|
|
5
|
+
const options = {
|
|
6
|
+
install: !optionsFromProgram.skipInstall,
|
|
7
|
+
};
|
|
8
|
+
if (optionsFromProgram.skipInstall === undefined) {
|
|
9
|
+
options.install = await confirm({
|
|
10
|
+
message: `Would you like to ${color.blue("Install dependencies")}?`,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
return options;
|
|
14
|
+
};
|