create-vitnode-app 2.0.0-canary.1 → 2.0.0-canary.11
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/README.md +9 -2
- package/copy-of-vitnode-app/README.md +22 -0
- package/copy-of-vitnode-app/api/src/vitnode.api.config.ts +10 -11
- package/copy-of-vitnode-app/api-single-app/src/routes/api/$.ts +4 -26
- package/copy-of-vitnode-app/api-single-app/src/server/api-bridge.ts +1 -12
- package/copy-of-vitnode-app/api-single-app/src/server/vitnode-api.server.ts +13 -13
- package/copy-of-vitnode-app/api-single-app/src/vitnode.api.config.ts +3 -12
- package/copy-of-vitnode-app/docker/docker-compose.yml +6 -4
- package/copy-of-vitnode-app/monorepo/apps/api/.env.example +1 -1
- package/copy-of-vitnode-app/monorepo/apps/web/.env.example +1 -12
- package/copy-of-vitnode-app/monorepo/turbo.json +8 -3
- package/copy-of-vitnode-app/root/.env.example +1 -8
- package/copy-of-vitnode-app/root/global.d.ts +1 -19
- package/copy-of-vitnode-app/root/src/components/admin-shell.tsx +2 -53
- package/copy-of-vitnode-app/root/src/components/main-footer.tsx +44 -0
- package/copy-of-vitnode-app/root/src/components/main-header.tsx +1 -30
- package/copy-of-vitnode-app/root/src/lib/i18n.ts +37 -0
- package/copy-of-vitnode-app/root/src/locales/app.ts +1 -25
- package/copy-of-vitnode-app/root/src/locales/packages.ts +1 -34
- package/copy-of-vitnode-app/root/src/router.tsx +16 -165
- package/copy-of-vitnode-app/root/src/routes/__root.tsx +12 -115
- package/copy-of-vitnode-app/root/src/routes/_admin/admin.core.index.tsx +4 -54
- package/copy-of-vitnode-app/root/src/routes/_admin.tsx +5 -198
- package/copy-of-vitnode-app/root/src/routes/_main/index.tsx +182 -46
- package/copy-of-vitnode-app/root/src/routes/_main.tsx +12 -47
- package/copy-of-vitnode-app/root/src/server/messages.server.ts +2 -23
- package/copy-of-vitnode-app/root/src/start.ts +3 -86
- package/copy-of-vitnode-app/root/src/styles.css +70 -92
- package/copy-of-vitnode-app/root/src/vitnode.config.ts +15 -53
- package/copy-of-vitnode-app/root/src/vitnode.server.config.ts +12 -0
- package/copy-of-vitnode-app/root/tsconfig.json +8 -3
- package/copy-of-vitnode-app/root/vite.config.ts +2 -99
- package/copy-of-vitnode-plugin/root/global.d.ts +6 -6
- package/dist/src/create/create-package-json.js +5 -4
- package/dist/src/create/create-vitnode.js +15 -1
- package/dist/src/create/package-versions.js +22 -22
- package/dist/src/helpers/init-git.js +38 -0
- package/dist/src/helpers/init-vitnode.js +2 -3
- package/dist/src/helpers/install-dependencies.js +2 -3
- package/dist/src/helpers/spawn-command.js +7 -0
- package/dist/src/index.js +1 -0
- package/dist/src/plugin/create/add-plugin-to-config.js +231 -0
- package/dist/src/plugin/create/create-package-json.js +12 -10
- package/dist/src/plugin/create/create-plugin-vitnode.js +23 -2
- package/dist/src/plugin/create/route-templates.js +103 -61
- package/dist/src/questions.js +7 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/copy-of-vitnode-app/api/src/i18n.ts +0 -38
- package/copy-of-vitnode-app/root/src/i18n.ts +0 -36
- package/copy-of-vitnode-app/root/src/lib/admin-auth.ts +0 -11
- package/copy-of-vitnode-app/root/src/lib/admin-nav.ts +0 -43
- package/copy-of-vitnode-app/root/src/lib/admin-search.ts +0 -37
- package/copy-of-vitnode-app/root/src/lib/auth.ts +0 -66
- package/copy-of-vitnode-app/root/src/lib/content-registry.ts +0 -51
- package/copy-of-vitnode-app/root/src/lib/document-headers.ts +0 -128
- package/copy-of-vitnode-app/root/src/lib/i18n/runtime.ts +0 -75
- package/copy-of-vitnode-app/root/src/lib/i18n/shared.ts +0 -20
- package/copy-of-vitnode-app/root/src/lib/navigation.ts +0 -24
- package/copy-of-vitnode-app/root/src/lib/page-head.ts +0 -19
- package/copy-of-vitnode-app/root/src/vitnode.shell.config.ts +0 -35
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import color from "picocolors";
|
|
2
|
+
import { spawnCommand } from "./spawn-command.js";
|
|
3
3
|
export const generateMigrationsVitnode = async ({ packageManager: pm, cwd, }) => {
|
|
4
4
|
const packageManager = pm.split("@")[0];
|
|
5
5
|
const args = ["vitnode", "migrate", "--generate"];
|
|
6
6
|
await new Promise((resolve, reject) => {
|
|
7
7
|
let output = "";
|
|
8
|
-
const child =
|
|
8
|
+
const child = spawnCommand(packageManager, args, {
|
|
9
9
|
cwd,
|
|
10
10
|
env: process.env,
|
|
11
|
-
shell: true,
|
|
12
11
|
stdio: "pipe",
|
|
13
12
|
});
|
|
14
13
|
child.stdout?.on("data", (data) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import consoleColor from "picocolors";
|
|
3
2
|
import { getOnline } from "./is-online.js";
|
|
3
|
+
import { spawnCommand } from "./spawn-command.js";
|
|
4
4
|
function printInstallErrorSuggestions(stderr, color) {
|
|
5
5
|
if (stderr.includes("ENOTFOUND") || stderr.includes("network")) {
|
|
6
6
|
console.error(color.yellow("💡 Network error detected. Please check your internet connection."));
|
|
@@ -27,10 +27,9 @@ export const installDependencies = async ({ packageManager: pm, cwd, }) => {
|
|
|
27
27
|
return new Promise((resolve, reject) => {
|
|
28
28
|
let stdout = "";
|
|
29
29
|
let stderr = "";
|
|
30
|
-
const child =
|
|
30
|
+
const child = spawnCommand(packageManager, args, {
|
|
31
31
|
stdio: "pipe",
|
|
32
32
|
cwd,
|
|
33
|
-
shell: true,
|
|
34
33
|
env: {
|
|
35
34
|
...process.env,
|
|
36
35
|
ADBLOCK: "1",
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export const spawnCommand = (command, args = [], options = {}) => {
|
|
3
|
+
const spawnOptions = { ...options, shell: false };
|
|
4
|
+
return process.platform === "win32"
|
|
5
|
+
? spawn("cmd.exe", ["/c", command, ...args], spawnOptions)
|
|
6
|
+
: spawn(command, args, spawnOptions);
|
|
7
|
+
};
|
package/dist/src/index.js
CHANGED
|
@@ -35,6 +35,7 @@ const init = async () => {
|
|
|
35
35
|
program.addOption(new Option("--package-manager <package-manager>", "Specify the package manager to use").choices(["npm", "pnpm", "bun"]));
|
|
36
36
|
program.option("--eslint", "Initialize with ESLint & Prettier config.");
|
|
37
37
|
program.option("--skip-install", "Skip installing packages after initializing the project.");
|
|
38
|
+
program.option("--skip-git", "Skip initializing a git repository in the project.");
|
|
38
39
|
program.addOption(new Option("--mode <mode>", "What type of app do you want to create?").choices(["singleApp", "apiMonorepo", "onlyApi"]));
|
|
39
40
|
program.option("--monorepo", "Create project with monorepo structure.");
|
|
40
41
|
program.option("--docker", "Initialize with Docker support.");
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { readdir, readFile, writeFile } from "fs/promises";
|
|
2
|
+
import { basename, dirname, join } from "path";
|
|
3
|
+
import { pluginApiVariableName, pluginVariableName, } from "./route-templates.js";
|
|
4
|
+
const SKIPPED_DIRECTORIES = new Set([
|
|
5
|
+
".git",
|
|
6
|
+
".next",
|
|
7
|
+
".nitro",
|
|
8
|
+
".output",
|
|
9
|
+
".turbo",
|
|
10
|
+
".vercel",
|
|
11
|
+
"build",
|
|
12
|
+
"coverage",
|
|
13
|
+
"dist",
|
|
14
|
+
"node_modules",
|
|
15
|
+
]);
|
|
16
|
+
const CONFIG_BUILDERS = {
|
|
17
|
+
"vitnode.api.config.ts": {
|
|
18
|
+
builder: "buildApiConfig",
|
|
19
|
+
subpath: "config.api",
|
|
20
|
+
},
|
|
21
|
+
"vitnode.config.ts": { builder: "buildConfig", subpath: "config" },
|
|
22
|
+
};
|
|
23
|
+
const endOfStringLiteral = (source, start) => {
|
|
24
|
+
const quote = source[start];
|
|
25
|
+
let index = start + 1;
|
|
26
|
+
while (index < source.length) {
|
|
27
|
+
const char = source[index];
|
|
28
|
+
if (char === "\\") {
|
|
29
|
+
index += 2;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (char === quote)
|
|
33
|
+
return index + 1;
|
|
34
|
+
index += 1;
|
|
35
|
+
}
|
|
36
|
+
return source.length;
|
|
37
|
+
};
|
|
38
|
+
const closingBracketOf = (source, open) => {
|
|
39
|
+
let depth = 0;
|
|
40
|
+
let index = open;
|
|
41
|
+
while (index < source.length) {
|
|
42
|
+
const char = source[index];
|
|
43
|
+
if (char === "/" && source[index + 1] === "/") {
|
|
44
|
+
const newline = source.indexOf("\n", index);
|
|
45
|
+
index = newline === -1 ? source.length : newline;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (char === "/" && source[index + 1] === "*") {
|
|
49
|
+
const end = source.indexOf("*/", index + 2);
|
|
50
|
+
index = end === -1 ? source.length : end + 2;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
54
|
+
index = endOfStringLiteral(source, index);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (char === "[" || char === "(" || char === "{") {
|
|
58
|
+
depth += 1;
|
|
59
|
+
}
|
|
60
|
+
else if (char === "]" || char === ")" || char === "}") {
|
|
61
|
+
depth -= 1;
|
|
62
|
+
if (depth === 0)
|
|
63
|
+
return char === "]" ? index : -1;
|
|
64
|
+
}
|
|
65
|
+
index += 1;
|
|
66
|
+
}
|
|
67
|
+
return -1;
|
|
68
|
+
};
|
|
69
|
+
const findPluginsArray = (source, from) => {
|
|
70
|
+
const key = /\bplugins\s*:\s*\[/g;
|
|
71
|
+
key.lastIndex = from;
|
|
72
|
+
const match = key.exec(source);
|
|
73
|
+
if (!match)
|
|
74
|
+
return null;
|
|
75
|
+
const open = match.index + match[0].length - 1;
|
|
76
|
+
const close = closingBracketOf(source, open);
|
|
77
|
+
return close === -1 ? null : { close, open };
|
|
78
|
+
};
|
|
79
|
+
const readImports = (source) => {
|
|
80
|
+
const statements = [];
|
|
81
|
+
let offset = 0;
|
|
82
|
+
let open = null;
|
|
83
|
+
for (const line of source.split("\n")) {
|
|
84
|
+
const lineStart = offset;
|
|
85
|
+
offset += line.length + 1;
|
|
86
|
+
open ??= /^import\b/.test(line)
|
|
87
|
+
? { isType: /^import\s+type\b/.test(line), start: lineStart }
|
|
88
|
+
: null;
|
|
89
|
+
if (!open)
|
|
90
|
+
continue;
|
|
91
|
+
const specifier = /\bfrom\s+['"]([^'"]+)['"]/.exec(line)?.[1] ??
|
|
92
|
+
/^import\s+['"]([^'"]+)['"]/.exec(line)?.[1];
|
|
93
|
+
if (specifier === undefined)
|
|
94
|
+
continue;
|
|
95
|
+
const end = lineStart + line.length;
|
|
96
|
+
statements.push({
|
|
97
|
+
end,
|
|
98
|
+
isType: open.isType,
|
|
99
|
+
specifier,
|
|
100
|
+
start: open.start,
|
|
101
|
+
text: source.slice(open.start, end),
|
|
102
|
+
});
|
|
103
|
+
open = null;
|
|
104
|
+
}
|
|
105
|
+
return statements;
|
|
106
|
+
};
|
|
107
|
+
const importStyleOf = (source) => {
|
|
108
|
+
const match = /\bfrom\s+(['"])[^'"\n]+\1(;?)/.exec(source);
|
|
109
|
+
return { quote: match?.[1] ?? '"', semicolon: match?.[2] ?? ";" };
|
|
110
|
+
};
|
|
111
|
+
const withImport = (source, { factory, module }) => {
|
|
112
|
+
const { quote, semicolon } = importStyleOf(source);
|
|
113
|
+
const statement = `import { ${factory} } from ${quote}${module}${quote}${semicolon}`;
|
|
114
|
+
const imports = readImports(source);
|
|
115
|
+
if (imports.length === 0) {
|
|
116
|
+
return `${statement}\n\n${source}`;
|
|
117
|
+
}
|
|
118
|
+
const before = imports.find(entry => !entry.isType && entry.specifier > module);
|
|
119
|
+
if (before) {
|
|
120
|
+
return `${source.slice(0, before.start)}${statement}\n${source.slice(before.start)}`;
|
|
121
|
+
}
|
|
122
|
+
const last = imports[imports.length - 1];
|
|
123
|
+
return `${source.slice(0, last.end)}\n${statement}${source.slice(last.end)}`;
|
|
124
|
+
};
|
|
125
|
+
const withEntry = (source, { close, open }, call) => {
|
|
126
|
+
const body = source.slice(open + 1, close);
|
|
127
|
+
const head = source.slice(0, open + 1);
|
|
128
|
+
const tail = source.slice(close);
|
|
129
|
+
if (body.trim() === "") {
|
|
130
|
+
if (!body.includes("\n"))
|
|
131
|
+
return `${head}${call}${tail}`;
|
|
132
|
+
const indent = /\n([ \t]*)$/.exec(body)?.[1] ?? "";
|
|
133
|
+
return `${head}\n${indent} ${call},${body}${tail}`;
|
|
134
|
+
}
|
|
135
|
+
if (!body.includes("\n")) {
|
|
136
|
+
const separator = body.trimEnd().endsWith(",") ? " " : ", ";
|
|
137
|
+
return `${head}${body}${separator}${call}${tail}`;
|
|
138
|
+
}
|
|
139
|
+
const entries = body.trimEnd();
|
|
140
|
+
const trailing = body.slice(entries.length);
|
|
141
|
+
const indent = /^[ \t]*(?=\S)/m.exec(body.slice(body.indexOf("\n") + 1))?.[0] ?? " ";
|
|
142
|
+
const comma = entries.endsWith(",") ? "" : ",";
|
|
143
|
+
return `${head}${entries}${comma}\n${indent}${call},${trailing}${tail}`;
|
|
144
|
+
};
|
|
145
|
+
export const registerPluginInSource = (source, { builder, factory, module }) => {
|
|
146
|
+
const builderAt = source.indexOf(`${builder}(`);
|
|
147
|
+
if (builderAt === -1)
|
|
148
|
+
return { source, status: "no-config-call" };
|
|
149
|
+
const range = findPluginsArray(source, builderAt);
|
|
150
|
+
if (!range)
|
|
151
|
+
return { source, status: "no-plugins-array" };
|
|
152
|
+
const called = new RegExp(`\\b${factory}\\s*\\(`);
|
|
153
|
+
const hasEntry = called.test(source.slice(range.open + 1, range.close));
|
|
154
|
+
const hasImport = readImports(source).some(entry => entry.specifier === module &&
|
|
155
|
+
new RegExp(`\\b${factory}\\b`).test(entry.text));
|
|
156
|
+
if (hasEntry && hasImport)
|
|
157
|
+
return { source, status: "already-registered" };
|
|
158
|
+
const withCall = hasEntry ? source : withEntry(source, range, `${factory}()`);
|
|
159
|
+
return {
|
|
160
|
+
source: hasImport ? withCall : withImport(withCall, { factory, module }),
|
|
161
|
+
status: "registered",
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
const readEntries = async (dir) => {
|
|
165
|
+
try {
|
|
166
|
+
return await readdir(dir, { withFileTypes: true });
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const findConfigFiles = async (dir, skipDir, results = []) => {
|
|
173
|
+
for (const entry of await readEntries(dir)) {
|
|
174
|
+
const fullPath = join(dir, entry.name);
|
|
175
|
+
if (entry.isDirectory()) {
|
|
176
|
+
if (SKIPPED_DIRECTORIES.has(entry.name) || fullPath === skipDir)
|
|
177
|
+
continue;
|
|
178
|
+
await findConfigFiles(fullPath, skipDir, results);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (entry.isFile() && CONFIG_BUILDERS[entry.name]) {
|
|
182
|
+
results.push(fullPath);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return results;
|
|
186
|
+
};
|
|
187
|
+
const ownedByVitNodePackage = async (file, rootPath) => {
|
|
188
|
+
let current = dirname(file);
|
|
189
|
+
while (current.startsWith(rootPath)) {
|
|
190
|
+
try {
|
|
191
|
+
const pkg = JSON.parse(await readFile(join(current, "package.json"), "utf-8"));
|
|
192
|
+
return Boolean(pkg.dependencies?.["@vitnode/core"] ??
|
|
193
|
+
pkg.devDependencies?.["@vitnode/core"]);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
const parent = dirname(current);
|
|
197
|
+
if (parent === current)
|
|
198
|
+
return false;
|
|
199
|
+
current = parent;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
};
|
|
204
|
+
export const addPluginToConfig = async ({ pluginName, pluginPath, rootPath, }) => {
|
|
205
|
+
const files = await findConfigFiles(rootPath, pluginPath);
|
|
206
|
+
const registrations = [];
|
|
207
|
+
for (const file of files.sort()) {
|
|
208
|
+
if (!(await ownedByVitNodePackage(file, rootPath)))
|
|
209
|
+
continue;
|
|
210
|
+
const target = CONFIG_BUILDERS[basename(file)];
|
|
211
|
+
const isApi = target.subpath === "config.api";
|
|
212
|
+
try {
|
|
213
|
+
const source = await readFile(file, "utf-8");
|
|
214
|
+
const { source: next, status } = registerPluginInSource(source, {
|
|
215
|
+
builder: target.builder,
|
|
216
|
+
factory: isApi
|
|
217
|
+
? pluginApiVariableName(pluginName)
|
|
218
|
+
: pluginVariableName(pluginName),
|
|
219
|
+
module: `${pluginName}/${target.subpath}`,
|
|
220
|
+
});
|
|
221
|
+
if (status === "registered") {
|
|
222
|
+
await writeFile(file, next, "utf-8");
|
|
223
|
+
}
|
|
224
|
+
registrations.push({ file, status });
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return registrations;
|
|
231
|
+
};
|
|
@@ -4,6 +4,15 @@ import { getVitnodePackageVersion } from "../../helpers/get-vitnode-package-vers
|
|
|
4
4
|
import { withIf } from "../../helpers/with-If.js";
|
|
5
5
|
import { pluginPackageExports } from "./route-templates.js";
|
|
6
6
|
const writeJson = async (path, data) => writeFile(path, JSON.stringify(data, null, 2));
|
|
7
|
+
export const pluginScripts = (eslint) => ({
|
|
8
|
+
"build:plugins": "vitnode build",
|
|
9
|
+
dev: "vitnode dev",
|
|
10
|
+
"dev:email": "email dev --dir src/emails",
|
|
11
|
+
...withIf(eslint, {
|
|
12
|
+
lint: "turbo lint",
|
|
13
|
+
"lint:fix": "turbo lint:fix",
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
7
16
|
export const createPluginPackageJSON = async ({ pluginName, pluginPath, eslint, }) => {
|
|
8
17
|
const vitnodeVersionRange = await getVitnodePackageVersion();
|
|
9
18
|
const pluginPkg = {
|
|
@@ -11,18 +20,11 @@ export const createPluginPackageJSON = async ({ pluginName, pluginPath, eslint,
|
|
|
11
20
|
version: "0.1.0",
|
|
12
21
|
private: true,
|
|
13
22
|
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
|
+
scripts: pluginScripts(eslint),
|
|
23
24
|
exports: pluginPackageExports(),
|
|
24
25
|
dependencies: {
|
|
25
26
|
"@hono/zod-openapi": versionsPackageJson.honoZodOpenapi,
|
|
27
|
+
"@tanstack/react-form": versionsPackageJson.tanstackReactForm,
|
|
26
28
|
"@vitnode/core": vitnodeVersionRange,
|
|
27
29
|
"drizzle-kit": versionsPackageJson.drizzleKit,
|
|
28
30
|
"drizzle-orm": versionsPackageJson.drizzleOrm,
|
|
@@ -31,7 +33,6 @@ export const createPluginPackageJSON = async ({ pluginName, pluginPath, eslint,
|
|
|
31
33
|
react: versionsPackageJson.react,
|
|
32
34
|
"react-dom": versionsPackageJson.reactDom,
|
|
33
35
|
"react-email": versionsPackageJson.reactEmail,
|
|
34
|
-
"react-hook-form": versionsPackageJson.rhf,
|
|
35
36
|
sonner: versionsPackageJson.sonner,
|
|
36
37
|
"use-intl": versionsPackageJson.useIntl,
|
|
37
38
|
zod: versionsPackageJson.zod,
|
|
@@ -43,6 +44,7 @@ export const createPluginPackageJSON = async ({ pluginName, pluginPath, eslint,
|
|
|
43
44
|
"@types/react": versionsPackageJson.typesReact,
|
|
44
45
|
"@types/react-dom": versionsPackageJson.typesReactDom,
|
|
45
46
|
"@vitnode/config": vitnodeVersionRange,
|
|
47
|
+
cn: versionsPackageJson.cn,
|
|
46
48
|
...withIf(eslint, {
|
|
47
49
|
eslint: versionsPackageJson.eslint,
|
|
48
50
|
}),
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { existsSync } from "fs";
|
|
2
2
|
import { cp, mkdir, rename, writeFile } from "fs/promises";
|
|
3
3
|
import ora from "ora";
|
|
4
|
-
import { dirname, join } from "path";
|
|
4
|
+
import { dirname, join, relative } from "path";
|
|
5
5
|
import color from "picocolors";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import { getPackageManagerFromRoot } from "../../helpers/get-package-manager-from-root.js";
|
|
8
8
|
import { installDependencies } from "../../helpers/install-dependencies.js";
|
|
9
9
|
import { isFolderEmpty } from "../../helpers/is-folder-empty.js";
|
|
10
|
+
import { addPluginToConfig } from "./add-plugin-to-config.js";
|
|
10
11
|
import { addPluginToWorkspace } from "./add-plugin-to-workspace.js";
|
|
11
12
|
import { createPluginPackageJSON } from "./create-package-json.js";
|
|
12
13
|
import { pluginRouteScaffold } from "./route-templates.js";
|
|
@@ -15,6 +16,19 @@ const writePluginRouteScaffold = async ({ pluginName, pluginPath, }) => {
|
|
|
15
16
|
await Promise.all(Object.keys(files).map(async (file) => mkdir(dirname(join(pluginPath, file)), { recursive: true })));
|
|
16
17
|
await Promise.all(Object.entries(files).map(async ([file, contents]) => writeFile(join(pluginPath, file), contents, "utf-8")));
|
|
17
18
|
};
|
|
19
|
+
const reportConfigRegistrations = ({ pluginName, registrations, rootPath, }) => {
|
|
20
|
+
const registered = registrations.filter(({ status }) => status === "registered");
|
|
21
|
+
registered.forEach(({ file }) => {
|
|
22
|
+
console.log(` ${color.green("+")} Registered in ${color.cyan(relative(rootPath, file))}`);
|
|
23
|
+
});
|
|
24
|
+
const unusable = registrations.filter(({ status }) => status === "no-plugins-array");
|
|
25
|
+
unusable.forEach(({ file }) => {
|
|
26
|
+
console.log(` ${color.yellow("!")} ${color.cyan(relative(rootPath, file))} has no \`plugins\` array - add ${color.cyan(pluginName)} to it by hand.`);
|
|
27
|
+
});
|
|
28
|
+
if (registered.length === 0 && unusable.length === 0) {
|
|
29
|
+
console.log(` ${color.yellow("!")} No VitNode config found. Add ${color.cyan(`${pluginName}/config`)} to your app's \`vitnode.config.ts\` and ${color.cyan(`${pluginName}/config.api`)} to its \`vitnode.api.config.ts\`.`);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
18
32
|
export const createPluginVitNode = async ({ pluginPath, pluginName, install, eslint, }) => {
|
|
19
33
|
const packageManager = getPackageManagerFromRoot(process.cwd());
|
|
20
34
|
const spinner = ora(`Creating a new VitNode plugin in ${color.green(pluginPath)}. Using ${color.green(packageManager)}...`).start();
|
|
@@ -36,7 +50,7 @@ export const createPluginVitNode = async ({ pluginPath, pluginName, install, esl
|
|
|
36
50
|
if (existsSync(npmIgnoreTemplatePath)) {
|
|
37
51
|
await rename(npmIgnoreTemplatePath, dotNpmIgnorePath);
|
|
38
52
|
}
|
|
39
|
-
spinner.text = "Writing the plugin's first route...";
|
|
53
|
+
spinner.text = "Writing the plugin's first route and API...";
|
|
40
54
|
await writePluginRouteScaffold({ pluginName, pluginPath });
|
|
41
55
|
spinner.text = "Creating package.json...";
|
|
42
56
|
await createPluginPackageJSON({
|
|
@@ -69,6 +83,12 @@ export const createPluginVitNode = async ({ pluginPath, pluginName, install, esl
|
|
|
69
83
|
pluginPath,
|
|
70
84
|
rootPath,
|
|
71
85
|
});
|
|
86
|
+
spinner.text = "Registering the plugin with the apps that can serve it...";
|
|
87
|
+
const registrations = await addPluginToConfig({
|
|
88
|
+
pluginName,
|
|
89
|
+
pluginPath,
|
|
90
|
+
rootPath,
|
|
91
|
+
});
|
|
72
92
|
if (install) {
|
|
73
93
|
spinner.text = "Installing dependencies...";
|
|
74
94
|
await installDependencies({
|
|
@@ -77,4 +97,5 @@ export const createPluginVitNode = async ({ pluginPath, pluginName, install, esl
|
|
|
77
97
|
});
|
|
78
98
|
}
|
|
79
99
|
spinner.succeed(`${color.green("Success!")} Created ${color.cyan(pluginName)} at ${color.cyan(pluginPath)}`);
|
|
100
|
+
reportConfigRegistrations({ pluginName, registrations, rootPath });
|
|
80
101
|
};
|
|
@@ -1,27 +1,14 @@
|
|
|
1
1
|
export const routeSlugFor = (pluginName) => pluginName.includes("/")
|
|
2
2
|
? pluginName.slice(pluginName.indexOf("/") + 1)
|
|
3
3
|
: pluginName;
|
|
4
|
+
export const pluginConstTemplate = (pluginName) => `export const CONFIG_PLUGIN = {
|
|
5
|
+
pluginId: "${pluginName}" as const,
|
|
6
|
+
};
|
|
7
|
+
`;
|
|
4
8
|
export const pluginRoutesTemplate = (pluginName) => {
|
|
5
9
|
const slug = routeSlugFor(pluginName);
|
|
6
10
|
return `import { definePluginRoutes, lazy, page } from "@vitnode/core/routing";
|
|
7
11
|
|
|
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
12
|
export const routes = definePluginRoutes([
|
|
26
13
|
page("/${slug}", {
|
|
27
14
|
component: lazy(() => import("./pages/home-page")),
|
|
@@ -29,33 +16,30 @@ export const routes = definePluginRoutes([
|
|
|
29
16
|
]);
|
|
30
17
|
`;
|
|
31
18
|
};
|
|
32
|
-
export const pluginRouteModuleTemplate = (pluginName) => `import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
* });
|
|
57
|
-
*/
|
|
58
|
-
const HomePage = () => {
|
|
19
|
+
export const pluginRouteModuleTemplate = (pluginName) => `import type { PluginRoutePageProps } from "@vitnode/core/routing";
|
|
20
|
+
|
|
21
|
+
import { definePluginRoute } from "@vitnode/core/routing";
|
|
22
|
+
import { useTranslations } from "use-intl";
|
|
23
|
+
|
|
24
|
+
import { helloApi } from "@/api/client";
|
|
25
|
+
|
|
26
|
+
interface HelloMessage {
|
|
27
|
+
message: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const route = definePluginRoute<HelloMessage>({
|
|
31
|
+
load: async () => {
|
|
32
|
+
const response = await helloApi.fetch({
|
|
33
|
+
method: "get",
|
|
34
|
+
module: "hello",
|
|
35
|
+
path: "/",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return await response.json();
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const HomePage = ({ loaderData }: PluginRoutePageProps<HelloMessage>) => {
|
|
59
43
|
const t = useTranslations("${pluginName}");
|
|
60
44
|
|
|
61
45
|
return (
|
|
@@ -67,6 +51,11 @@ const HomePage = () => {
|
|
|
67
51
|
<p className="text-muted-foreground leading-relaxed text-pretty">
|
|
68
52
|
{t("home.desc")}
|
|
69
53
|
</p>
|
|
54
|
+
|
|
55
|
+
<div className="bg-card text-card-foreground flex flex-col gap-1 rounded-lg border p-4">
|
|
56
|
+
<span className="text-muted-foreground text-sm">{t("home.api")}</span>
|
|
57
|
+
<span className="font-medium">{loaderData.message}</span>
|
|
58
|
+
</div>
|
|
70
59
|
</div>
|
|
71
60
|
);
|
|
72
61
|
};
|
|
@@ -76,6 +65,7 @@ export default HomePage;
|
|
|
76
65
|
export const pluginMessagesTemplate = (pluginName) => `${JSON.stringify({
|
|
77
66
|
[pluginName]: {
|
|
78
67
|
home: {
|
|
68
|
+
api: "Your plugin's API answered:",
|
|
79
69
|
desc: "This page ships inside the plugin and is served by the app that installed it.",
|
|
80
70
|
title: "Hello from your plugin",
|
|
81
71
|
},
|
|
@@ -83,11 +73,6 @@ export const pluginMessagesTemplate = (pluginName) => `${JSON.stringify({
|
|
|
83
73
|
}, null, 2)}\n`;
|
|
84
74
|
export const pluginMessagesBarrelTemplate = () => `import type { LocaleMessagesMap } from "@vitnode/core/lib/i18n/types";
|
|
85
75
|
|
|
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
76
|
const messages: LocaleMessagesMap = {
|
|
92
77
|
en: async () => await import("./en.json", { with: { type: "json" } }),
|
|
93
78
|
};
|
|
@@ -106,27 +91,79 @@ export const pluginVariableName = (pluginName) => {
|
|
|
106
91
|
};
|
|
107
92
|
export const pluginConfigTemplate = (pluginName) => `import { buildPlugin } from "@vitnode/core/lib/plugin";
|
|
108
93
|
|
|
94
|
+
import { CONFIG_PLUGIN } from "@/const";
|
|
95
|
+
|
|
109
96
|
import messages from "./locales";
|
|
110
97
|
import { routes } from "./routes";
|
|
111
98
|
|
|
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
99
|
export const ${pluginVariableName(pluginName)} = () =>
|
|
124
100
|
buildPlugin({
|
|
125
|
-
pluginId:
|
|
101
|
+
pluginId: CONFIG_PLUGIN.pluginId,
|
|
126
102
|
messages,
|
|
127
103
|
routes,
|
|
128
104
|
});
|
|
129
105
|
`;
|
|
106
|
+
export const pluginApiVariableName = (pluginName) => pluginVariableName(pluginName).replace(/Plugin$/, "ApiPlugin");
|
|
107
|
+
export const pluginApiRouteTemplate = () => `import { z } from "@hono/zod-openapi";
|
|
108
|
+
import { buildRoute } from "@vitnode/core/api/lib/route";
|
|
109
|
+
|
|
110
|
+
import { CONFIG_PLUGIN } from "@/const";
|
|
111
|
+
|
|
112
|
+
export const helloRoute = buildRoute({
|
|
113
|
+
pluginId: CONFIG_PLUGIN.pluginId,
|
|
114
|
+
route: {
|
|
115
|
+
method: "get",
|
|
116
|
+
path: "/",
|
|
117
|
+
responses: {
|
|
118
|
+
200: {
|
|
119
|
+
content: {
|
|
120
|
+
"application/json": {
|
|
121
|
+
schema: z.object({ message: z.string() }),
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
description: "A greeting from the plugin.",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
handler: c => c.json({ message: \`Hello from \${CONFIG_PLUGIN.pluginId}!\` }),
|
|
129
|
+
});
|
|
130
|
+
`;
|
|
131
|
+
export const pluginApiModuleTemplate = () => `import { buildModule } from "@vitnode/core/api/lib/module";
|
|
132
|
+
|
|
133
|
+
import { CONFIG_PLUGIN } from "@/const";
|
|
134
|
+
|
|
135
|
+
import { helloRoute } from "./hello.route";
|
|
136
|
+
|
|
137
|
+
export const helloModule = buildModule({
|
|
138
|
+
pluginId: CONFIG_PLUGIN.pluginId,
|
|
139
|
+
name: "hello",
|
|
140
|
+
routes: [helloRoute],
|
|
141
|
+
});
|
|
142
|
+
`;
|
|
143
|
+
export const pluginApiClientTemplate = () => `import type { ApiClient } from "@vitnode/core/tanstack/fetcher";
|
|
144
|
+
|
|
145
|
+
import { createApiClient } from "@vitnode/core/tanstack/fetcher";
|
|
146
|
+
|
|
147
|
+
import type { helloModule } from "@/api/modules/hello/hello.module";
|
|
148
|
+
|
|
149
|
+
import { CONFIG_PLUGIN } from "@/const";
|
|
150
|
+
|
|
151
|
+
export const helloApi: ApiClient<typeof helloModule> = createApiClient<
|
|
152
|
+
typeof helloModule
|
|
153
|
+
>(CONFIG_PLUGIN.pluginId);
|
|
154
|
+
`;
|
|
155
|
+
export const pluginApiConfigTemplate = (pluginName) => `import { buildApiPlugin } from "@vitnode/core/api/lib/plugin";
|
|
156
|
+
|
|
157
|
+
import { CONFIG_PLUGIN } from "@/const";
|
|
158
|
+
|
|
159
|
+
import { helloModule } from "./api/modules/hello/hello.module";
|
|
160
|
+
|
|
161
|
+
export const ${pluginApiVariableName(pluginName)} = () =>
|
|
162
|
+
buildApiPlugin({
|
|
163
|
+
pluginId: CONFIG_PLUGIN.pluginId,
|
|
164
|
+
modules: [helloModule],
|
|
165
|
+
});
|
|
166
|
+
`;
|
|
130
167
|
export const pluginPackageExports = () => ({
|
|
131
168
|
"./locales/*.json": "./src/locales/*.json",
|
|
132
169
|
"./*": {
|
|
@@ -136,7 +173,12 @@ export const pluginPackageExports = () => ({
|
|
|
136
173
|
},
|
|
137
174
|
});
|
|
138
175
|
export const pluginRouteScaffold = (pluginName) => ({
|
|
176
|
+
"src/api/client.ts": pluginApiClientTemplate(),
|
|
177
|
+
"src/api/modules/hello/hello.module.ts": pluginApiModuleTemplate(),
|
|
178
|
+
"src/api/modules/hello/hello.route.ts": pluginApiRouteTemplate(),
|
|
179
|
+
"src/config.api.ts": pluginApiConfigTemplate(pluginName),
|
|
139
180
|
"src/config.tsx": pluginConfigTemplate(pluginName),
|
|
181
|
+
"src/const.ts": pluginConstTemplate(pluginName),
|
|
140
182
|
"src/locales/en.json": pluginMessagesTemplate(pluginName),
|
|
141
183
|
"src/locales/index.ts": pluginMessagesBarrelTemplate(),
|
|
142
184
|
"src/pages/home-page.tsx": pluginRouteModuleTemplate(pluginName),
|
package/dist/src/questions.js
CHANGED
|
@@ -6,6 +6,7 @@ export const createQuestionsCli = async (program) => {
|
|
|
6
6
|
const options = {
|
|
7
7
|
packageManager: optionsFromProgram.packageManager,
|
|
8
8
|
eslint: optionsFromProgram.eslint,
|
|
9
|
+
git: !optionsFromProgram.skipGit,
|
|
9
10
|
install: !optionsFromProgram.skipInstall,
|
|
10
11
|
docker: optionsFromProgram.docker,
|
|
11
12
|
mode: optionsFromProgram.mode,
|
|
@@ -74,6 +75,12 @@ export const createQuestionsCli = async (program) => {
|
|
|
74
75
|
message: `Would you like to use ${color.blue("Docker Container")}?`,
|
|
75
76
|
});
|
|
76
77
|
}
|
|
78
|
+
if (optionsFromProgram.skipGit === undefined) {
|
|
79
|
+
options.git = await confirm({
|
|
80
|
+
message: `Would you like to initialize a ${color.blue("Git repository")}?`,
|
|
81
|
+
default: true,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
77
84
|
if (optionsFromProgram.skipInstall === undefined) {
|
|
78
85
|
options.install = await confirm({
|
|
79
86
|
message: `Would you like to ${color.blue("Install dependencies")}?`,
|