create-vexcms 0.0.19 → 0.1.0-alpha.2
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 +202 -0
- package/README.md +93 -83
- package/dist/index.js +246 -81
- package/dist/index.mjs +30976 -0
- package/package.json +35 -15
- package/templates/base-nextjs/README.md +53 -205
- package/templates/base-nextjs/_env.example +7 -9
- package/templates/base-nextjs/_gitignore +41 -1
- package/templates/base-nextjs/convex/_generated/api.d.ts +6 -62
- package/templates/base-nextjs/convex/_generated/server.d.ts +19 -0
- package/templates/base-nextjs/convex/_generated/server.js +8 -0
- package/templates/base-nextjs/convex/auth/api.ts +49 -3
- package/templates/base-nextjs/convex/auth/db.ts +42 -300
- package/templates/base-nextjs/convex/auth/index.ts +11 -7
- package/templates/base-nextjs/convex/auth/options.ts +5 -29
- package/templates/base-nextjs/convex/auth/plugins/index.ts +15 -5
- package/templates/base-nextjs/convex/auth/sessions.ts +9 -15
- package/templates/base-nextjs/convex/http.ts +18 -3
- package/templates/base-nextjs/convex/schema.ts +104 -2
- package/templates/base-nextjs/convex/vex/firstUser.ts +10 -84
- package/templates/base-nextjs/convex/vex/globals.ts +19 -82
- package/templates/base-nextjs/convex/vex/media.ts +19 -246
- package/templates/base-nextjs/convex/vex.schema.ts +138 -0
- package/templates/base-nextjs/convex/vex.ts +19 -0
- package/templates/base-nextjs/convex.json +6 -0
- package/templates/base-nextjs/eslint.config.mjs +1 -21
- package/templates/base-nextjs/next.config.ts +10 -0
- package/templates/base-nextjs/package.json +47 -48
- package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +17 -5
- package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +2 -4
- package/templates/base-nextjs/src/app/(frontend)/layout.tsx +12 -2
- package/templates/base-nextjs/src/app/(frontend)/page.tsx +2 -113
- package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +40 -0
- package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +22 -0
- package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +19 -0
- package/templates/base-nextjs/src/app/globals.css +240 -104
- package/templates/base-nextjs/src/app/layout.tsx +23 -13
- package/templates/base-nextjs/src/app/unauthorized/page.tsx +24 -0
- package/templates/base-nextjs/src/auth/access.ts +46 -0
- package/templates/base-nextjs/src/auth/client.tsx +17 -5
- package/templates/base-nextjs/src/auth/hasPermission.ts +39 -0
- package/templates/base-nextjs/src/auth/serverUtils.ts +15 -22
- package/templates/base-nextjs/src/components/WelcomePage.tsx +116 -0
- package/templates/base-nextjs/src/components/component-example.tsx +56 -464
- package/templates/base-nextjs/src/components/providers/auth.tsx +13 -0
- package/templates/base-nextjs/src/components/providers/client.tsx +6 -1
- package/templates/base-nextjs/src/components/providers/convex.tsx +53 -21
- package/templates/base-nextjs/src/components/providers/server.tsx +10 -7
- package/templates/{marketing-site → base-nextjs}/src/components/ui/accordion.tsx +30 -32
- package/templates/base-nextjs/src/components/ui/badge.tsx +10 -13
- package/templates/base-nextjs/src/components/ui/input.tsx +35 -5
- package/templates/base-nextjs/src/context/AuthContext.tsx +29 -0
- package/templates/base-nextjs/src/db/constants/auth.ts +0 -1
- package/templates/base-nextjs/src/db/constants/index.ts +22 -35
- package/templates/base-nextjs/src/lib/utils.ts +0 -12
- package/templates/base-nextjs/src/proxy.ts +98 -11
- package/templates/base-nextjs/src/vex.config.ts +35 -0
- package/templates/base-nextjs/src/vex.types.ts +293 -0
- package/templates/base-nextjs/src/vexcms/collections/images.ts +15 -0
- package/templates/base-nextjs/src/vexcms/collections/index.ts +2 -1
- package/templates/base-nextjs/src/vexcms/collections/users.ts +7 -35
- package/templates/base-nextjs/tsconfig.json +3 -0
- package/templates/marketing-site/convex/_generated/api.d.ts +81 -0
- package/templates/marketing-site/convex/_generated/api.js +23 -0
- package/templates/marketing-site/convex/_generated/dataModel.d.ts +60 -0
- package/templates/marketing-site/convex/_generated/server.d.ts +162 -0
- package/templates/marketing-site/convex/_generated/server.js +101 -0
- package/templates/marketing-site/convex/footers.ts +11 -7
- package/templates/marketing-site/convex/headers.ts +15 -5
- package/templates/marketing-site/convex/pages.ts +20 -35
- package/templates/marketing-site/convex/schema.ts +119 -0
- package/templates/marketing-site/convex/seed.ts +582 -35
- package/templates/marketing-site/convex/siteSettings.ts +19 -5
- package/templates/marketing-site/convex/theme.ts +55 -61
- package/templates/marketing-site/convex/vex.schema.ts +355 -0
- package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +27 -160
- package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +6 -9
- package/templates/marketing-site/src/app/(frontend)/layout.tsx +4 -2
- package/templates/marketing-site/src/app/(frontend)/page.tsx +2 -5
- package/templates/marketing-site/src/components/SiteFooter.tsx +6 -3
- package/templates/marketing-site/src/components/SiteHeader.tsx +6 -3
- package/templates/marketing-site/src/components/ThemeLive.tsx +59 -0
- package/templates/marketing-site/src/components/ThemeStyle.tsx +45 -89
- package/templates/marketing-site/src/components/motion-primitives/animated-group.tsx +2 -2
- package/templates/marketing-site/src/lib/metadata.ts +51 -72
- package/templates/marketing-site/src/vex.config.ts +35 -0
- package/templates/marketing-site/src/vex.types.ts +634 -0
- package/templates/marketing-site/src/vexcms/api.ts +23 -0
- package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -3
- package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +2 -2
- package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -4
- package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +2 -2
- package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +4 -8
- package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +2 -2
- package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +7 -11
- package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +1 -1
- package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +9 -9
- package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +4 -5
- package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +3 -4
- package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +10 -19
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +4 -9
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +3 -6
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +28 -68
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +13 -43
- package/templates/marketing-site/src/vexcms/blocks/config.ts +7 -22
- package/templates/marketing-site/src/vexcms/blocks/constants.ts +2 -3
- package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -8
- package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -1
- package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -1
- package/templates/marketing-site/src/vexcms/collections/index.ts +2 -1
- package/templates/marketing-site/src/vexcms/collections/pages.ts +12 -21
- package/templates/marketing-site/src/vexcms/collections/themeColors.ts +118 -0
- package/templates/marketing-site/src/vexcms/collections/themes.ts +29 -111
- package/templates/marketing-site/src/vexcms/globals/index.ts +1 -1
- package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +43 -27
- package/templates/base-nextjs/convex/auth/adapter/index.ts +0 -230
- package/templates/base-nextjs/convex/auth/adapter/utils.ts +0 -547
- package/templates/base-nextjs/convex/auth/config.ts +0 -10
- package/templates/base-nextjs/convex/vex/auth.ts +0 -38
- package/templates/base-nextjs/convex/vex/collections.ts +0 -321
- package/templates/base-nextjs/convex/vex/helpers.ts +0 -33
- package/templates/base-nextjs/convex/vex/impersonation.ts +0 -51
- package/templates/base-nextjs/convex/vex/migrate.ts +0 -74
- package/templates/base-nextjs/convex/vex/model/collections.ts +0 -196
- package/templates/base-nextjs/convex/vex/model/media.ts +0 -87
- package/templates/base-nextjs/convex/vex/model/versions.ts +0 -254
- package/templates/base-nextjs/convex/vex/previewSnapshot.ts +0 -53
- package/templates/base-nextjs/convex/vex/versions.ts +0 -648
- package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +0 -33
- package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +0 -38
- package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +0 -101
- package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +0 -15
- package/templates/base-nextjs/src/app/admin/layout.tsx +0 -51
- package/templates/base-nextjs/src/app/favicon.ico +0 -0
- package/templates/base-nextjs/src/components/providers/theme.tsx +0 -11
- package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +0 -67
- package/templates/base-nextjs/src/db/types.ts +0 -6
- package/templates/base-nextjs/src/vexcms/access.ts +0 -44
- package/templates/base-nextjs/src/vexcms/auth.ts +0 -4
- package/templates/base-nextjs/src/vexcms/collections/media.ts +0 -15
- package/templates/base-nextjs/vex.config.ts +0 -25
- package/templates/marketing-site/public/favicons/favicon.ico +0 -0
- package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +0 -55
- package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +0 -27
- package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +0 -18
- package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +0 -71
- package/templates/marketing-site/src/components/ThemeInjector.tsx +0 -128
- package/templates/marketing-site/src/components/admin/IconPickerField.tsx +0 -134
- package/templates/marketing-site/src/components/admin/ThemeImport.tsx +0 -174
- package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +0 -33
- package/templates/marketing-site/src/db/constants/index.ts +0 -52
- package/templates/marketing-site/src/lib/colorConvert.ts +0 -171
- package/templates/marketing-site/src/vexcms/access.ts +0 -36
- package/templates/marketing-site/vex.config.ts +0 -32
package/dist/index.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import chalk2 from "chalk";
|
|
6
6
|
import { input, select, confirm, checkbox } from "@inquirer/prompts";
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import path3 from "path";
|
|
8
|
+
import fs6 from "fs-extra";
|
|
9
9
|
|
|
10
10
|
// src/utils/validation.ts
|
|
11
11
|
import validateNpmPackageName from "validate-npm-package-name";
|
|
@@ -32,11 +32,11 @@ async function isDirectoryEmpty(dirPath) {
|
|
|
32
32
|
const files = await fs.readdir(dirPath);
|
|
33
33
|
const visibleFiles = files.filter((file) => !file.startsWith("."));
|
|
34
34
|
return visibleFiles.length === 0;
|
|
35
|
-
} catch
|
|
35
|
+
} catch {
|
|
36
36
|
return true;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
function resolveProjectName(input2,
|
|
39
|
+
function resolveProjectName(input2, _cwd) {
|
|
40
40
|
return input2;
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -75,15 +75,60 @@ function displaySuccessMessage(projectName, targetPath, isCurrentDir) {
|
|
|
75
75
|
console.log();
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
// src/helpers/monorepo.ts
|
|
79
|
+
import fs2 from "fs-extra";
|
|
80
|
+
import path from "path";
|
|
81
|
+
import { parse as parseYaml } from "yaml";
|
|
82
|
+
import sortPackageJson from "sort-package-json";
|
|
83
|
+
async function findWorkspaceRoot(props) {
|
|
84
|
+
const { cwd } = props;
|
|
85
|
+
let dir = path.resolve(cwd);
|
|
86
|
+
while (true) {
|
|
87
|
+
if (await fs2.pathExists(path.join(dir, "pnpm-workspace.yaml"))) {
|
|
88
|
+
return dir;
|
|
89
|
+
}
|
|
90
|
+
const parent = path.dirname(dir);
|
|
91
|
+
if (parent === dir) return null;
|
|
92
|
+
dir = parent;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function readWorkspaceCatalog(props) {
|
|
96
|
+
const { workspaceRoot } = props;
|
|
97
|
+
const raw = await fs2.readFile(path.join(workspaceRoot, "pnpm-workspace.yaml"), "utf-8");
|
|
98
|
+
const parsed = parseYaml(raw);
|
|
99
|
+
return parsed.catalog ?? {};
|
|
100
|
+
}
|
|
101
|
+
var MONOREPO_DEPENDENCY_FIELDS = ["dependencies", "devDependencies"];
|
|
102
|
+
function rewriteManifestForMonorepo(props) {
|
|
103
|
+
const { manifest, catalog } = props;
|
|
104
|
+
const rewritten = { ...manifest };
|
|
105
|
+
for (const field of MONOREPO_DEPENDENCY_FIELDS) {
|
|
106
|
+
const deps = manifest[field];
|
|
107
|
+
if (!deps) continue;
|
|
108
|
+
const rewrittenDeps = {};
|
|
109
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
110
|
+
if (name.startsWith("@vexcms/")) {
|
|
111
|
+
rewrittenDeps[name] = "workspace:*";
|
|
112
|
+
} else if (name in catalog) {
|
|
113
|
+
rewrittenDeps[name] = "catalog:";
|
|
114
|
+
} else {
|
|
115
|
+
rewrittenDeps[name] = version;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
rewritten[field] = rewrittenDeps;
|
|
119
|
+
}
|
|
120
|
+
return sortPackageJson(rewritten);
|
|
121
|
+
}
|
|
122
|
+
|
|
78
123
|
// src/installers/base.ts
|
|
79
|
-
import
|
|
124
|
+
import fs4 from "fs-extra";
|
|
80
125
|
import { execa } from "execa";
|
|
81
126
|
import ora from "ora";
|
|
82
127
|
import crypto from "crypto";
|
|
83
|
-
import
|
|
128
|
+
import path2 from "path";
|
|
84
129
|
|
|
85
130
|
// src/helpers/fileOperations.ts
|
|
86
|
-
import
|
|
131
|
+
import fs3 from "fs-extra";
|
|
87
132
|
import { join, dirname } from "path";
|
|
88
133
|
import { fileURLToPath } from "url";
|
|
89
134
|
async function copyTemplate(framework, targetPath) {
|
|
@@ -91,10 +136,10 @@ async function copyTemplate(framework, targetPath) {
|
|
|
91
136
|
const __dirname = dirname(__filename);
|
|
92
137
|
const templateDir = framework === "tanstack" ? "base-tanstack" : "base-nextjs";
|
|
93
138
|
let templatePath = join(__dirname, "../../templates", templateDir);
|
|
94
|
-
if (!
|
|
139
|
+
if (!fs3.existsSync(templatePath)) {
|
|
95
140
|
templatePath = join(__dirname, "../templates", templateDir);
|
|
96
141
|
}
|
|
97
|
-
await
|
|
142
|
+
await fs3.copy(templatePath, targetPath, {
|
|
98
143
|
overwrite: false,
|
|
99
144
|
errorOnExist: false,
|
|
100
145
|
filter: (src) => {
|
|
@@ -105,8 +150,8 @@ async function copyTemplate(framework, targetPath) {
|
|
|
105
150
|
});
|
|
106
151
|
const sourceGitignore = join(templatePath, "_gitignore");
|
|
107
152
|
const targetGitignore = join(targetPath, ".gitignore");
|
|
108
|
-
if (await
|
|
109
|
-
await
|
|
153
|
+
if (await fs3.pathExists(sourceGitignore)) {
|
|
154
|
+
await fs3.copy(sourceGitignore, targetGitignore, {
|
|
110
155
|
overwrite: false,
|
|
111
156
|
errorOnExist: false
|
|
112
157
|
});
|
|
@@ -119,21 +164,30 @@ async function copyTemplate(framework, targetPath) {
|
|
|
119
164
|
for (const [from, to] of dotfileRenames) {
|
|
120
165
|
const source = join(templatePath, from);
|
|
121
166
|
const target = join(targetPath, to);
|
|
122
|
-
if (await
|
|
123
|
-
await
|
|
167
|
+
if (await fs3.pathExists(source)) {
|
|
168
|
+
await fs3.copy(source, target, { overwrite: false, errorOnExist: false });
|
|
124
169
|
}
|
|
125
170
|
}
|
|
126
171
|
}
|
|
127
172
|
async function overlayTemplate({ overlayDir, targetDir }) {
|
|
128
|
-
await
|
|
173
|
+
await fs3.copy(overlayDir, targetDir, { overwrite: true });
|
|
129
174
|
}
|
|
130
175
|
|
|
131
176
|
// src/installers/base.ts
|
|
132
177
|
var VexFrameworkInstaller = class {
|
|
178
|
+
/**
|
|
179
|
+
* Binds this installer to the destination directory and package name used
|
|
180
|
+
* throughout scaffolding (template copy target, package.json, README, etc).
|
|
181
|
+
*
|
|
182
|
+
* @param targetPath - Absolute path of the directory the project is created in.
|
|
183
|
+
* @param projectName - npm package name recorded for subclasses; not read directly by the base class.
|
|
184
|
+
*/
|
|
133
185
|
constructor(targetPath, projectName) {
|
|
134
186
|
this.targetPath = targetPath;
|
|
135
187
|
this.projectName = projectName;
|
|
136
188
|
}
|
|
189
|
+
targetPath;
|
|
190
|
+
projectName;
|
|
137
191
|
/**
|
|
138
192
|
* Copy base template files to target directory
|
|
139
193
|
*/
|
|
@@ -144,40 +198,52 @@ var VexFrameworkInstaller = class {
|
|
|
144
198
|
* Apply a template overlay on top of the base template.
|
|
145
199
|
* Merges overlay files onto the already-copied base — existing files
|
|
146
200
|
* not in the overlay are left untouched.
|
|
201
|
+
*
|
|
202
|
+
* @param overlay - Name of the overlay template directory under `templates/` (e.g. `"marketing-site"`).
|
|
147
203
|
*/
|
|
148
204
|
async applyTemplateOverlay(overlay) {
|
|
149
205
|
const { dirname: dirname2 } = await import("path");
|
|
150
206
|
const { fileURLToPath: fileURLToPath2 } = await import("url");
|
|
151
207
|
const __filename = fileURLToPath2(import.meta.url);
|
|
152
208
|
const __dirname = dirname2(__filename);
|
|
153
|
-
let overlayDir =
|
|
154
|
-
if (!await
|
|
155
|
-
overlayDir =
|
|
209
|
+
let overlayDir = path2.join(__dirname, "../../templates", overlay);
|
|
210
|
+
if (!await fs4.pathExists(overlayDir)) {
|
|
211
|
+
overlayDir = path2.join(__dirname, "../templates", overlay);
|
|
156
212
|
}
|
|
157
213
|
await overlayTemplate({ overlayDir, targetDir: this.targetPath });
|
|
158
214
|
}
|
|
159
215
|
/**
|
|
160
|
-
* Update the package.json name field
|
|
161
|
-
*
|
|
216
|
+
* Update the package.json name field, the vex:update script (to match the
|
|
217
|
+
* detected package manager), and README.md's `{{PROJECT_NAME}}` heading.
|
|
218
|
+
*
|
|
219
|
+
* @param name - npm package name to write into `package.json`'s `name` field
|
|
220
|
+
* and to substitute for `{{PROJECT_NAME}}` in `README.md`.
|
|
162
221
|
*/
|
|
163
222
|
async updatePackageName(name) {
|
|
164
|
-
const pkgPath =
|
|
165
|
-
const pkg = await
|
|
223
|
+
const pkgPath = path2.join(this.targetPath, "package.json");
|
|
224
|
+
const pkg = await fs4.readJson(pkgPath);
|
|
166
225
|
pkg.name = name;
|
|
167
226
|
const pm = this.detectPackageManager();
|
|
168
227
|
if (pkg.scripts?.["vex:update"] && pm !== "pnpm") {
|
|
169
228
|
const addCmd = pm === "yarn" ? "yarn add" : pm === "bun" ? "bun add" : "npm install";
|
|
170
229
|
pkg.scripts["vex:update"] = pkg.scripts["vex:update"].replace(/^pnpm add/, addCmd);
|
|
171
230
|
}
|
|
172
|
-
await
|
|
231
|
+
await fs4.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
232
|
+
const readmePath = path2.join(this.targetPath, "README.md");
|
|
233
|
+
if (await fs4.pathExists(readmePath)) {
|
|
234
|
+
const readme = await fs4.readFile(readmePath, "utf-8");
|
|
235
|
+
await fs4.writeFile(readmePath, readme.replace("{{PROJECT_NAME}}", name));
|
|
236
|
+
}
|
|
173
237
|
}
|
|
174
238
|
/**
|
|
175
239
|
* Configure the dev server port.
|
|
176
240
|
* Updates package.json dev script and creates .env.local with NEXT_PUBLIC_SITE_URL.
|
|
241
|
+
*
|
|
242
|
+
* @param port - Dev server port number to write into the `dev` script and `.env.local`.
|
|
177
243
|
*/
|
|
178
244
|
async configurePort(port) {
|
|
179
|
-
const pkgPath =
|
|
180
|
-
const pkg = await
|
|
245
|
+
const pkgPath = path2.join(this.targetPath, "package.json");
|
|
246
|
+
const pkg = await fs4.readJson(pkgPath);
|
|
181
247
|
if (pkg.scripts?.dev) {
|
|
182
248
|
if (pkg.scripts.dev.includes("--port")) {
|
|
183
249
|
pkg.scripts.dev = pkg.scripts.dev.replace(/--port[= ]\d+/, `--port=${port}`);
|
|
@@ -185,15 +251,42 @@ var VexFrameworkInstaller = class {
|
|
|
185
251
|
pkg.scripts.dev = `${pkg.scripts.dev} --port=${port}`;
|
|
186
252
|
}
|
|
187
253
|
}
|
|
188
|
-
await
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
254
|
+
await fs4.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
255
|
+
const siteUrl = `http://localhost:${port}`;
|
|
256
|
+
const envLocalPath = path2.join(this.targetPath, ".env.local");
|
|
257
|
+
const envContent = [
|
|
258
|
+
`NEXT_PUBLIC_SITE_URL=${siteUrl}`,
|
|
259
|
+
`SITE_URL=${siteUrl}`,
|
|
260
|
+
`BETTER_AUTH_SECRET=${this.generateAuthSecret()}`,
|
|
261
|
+
`NEXT_PUBLIC_CONVEX_URL=https://placeholder.convex.cloud`,
|
|
262
|
+
`NEXT_PUBLIC_CONVEX_SITE_URL=https://placeholder.convex.site`,
|
|
263
|
+
`CONVEX_DEPLOYMENT=`,
|
|
264
|
+
""
|
|
265
|
+
].join("\n");
|
|
266
|
+
await fs4.writeFile(envLocalPath, envContent);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Rewrites the scaffolded `package.json` for life inside a host pnpm
|
|
270
|
+
* workspace (`--monorepo`). `@vexcms/*` dependencies become
|
|
271
|
+
* `workspace:*`; any other dependency the host's `pnpm-workspace.yaml`
|
|
272
|
+
* catalog also pins becomes `catalog:`; everything else keeps its
|
|
273
|
+
* literal, catalog-resolved version. No-op when `options.monorepo` is
|
|
274
|
+
* false or `options.workspaceRoot` is unset.
|
|
275
|
+
*
|
|
276
|
+
* @param options - Resolved project options; only `monorepo`/`workspaceRoot` are consulted.
|
|
277
|
+
*/
|
|
278
|
+
async applyMonorepoRewrite(options) {
|
|
279
|
+
if (!options.monorepo || !options.workspaceRoot) return;
|
|
280
|
+
const pkgPath = path2.join(this.targetPath, "package.json");
|
|
281
|
+
const manifest = await fs4.readJson(pkgPath);
|
|
282
|
+
const catalog = await readWorkspaceCatalog({ workspaceRoot: options.workspaceRoot });
|
|
283
|
+
const rewritten = rewriteManifestForMonorepo({ manifest, catalog });
|
|
284
|
+
await fs4.writeJson(pkgPath, rewritten, { spaces: 2 });
|
|
194
285
|
}
|
|
195
286
|
/**
|
|
196
287
|
* Detect the package manager used to invoke the CLI
|
|
288
|
+
*
|
|
289
|
+
* @returns The package manager inferred from `npm_config_user_agent`, defaulting to `"npm"` when it can't be determined.
|
|
197
290
|
*/
|
|
198
291
|
detectPackageManager() {
|
|
199
292
|
const userAgent = process.env.npm_config_user_agent;
|
|
@@ -297,6 +390,8 @@ BETTER_AUTH_SECRET=""
|
|
|
297
390
|
}
|
|
298
391
|
/**
|
|
299
392
|
* Generate a secure random secret for Better Auth
|
|
393
|
+
*
|
|
394
|
+
* @returns A 64-character hex string (32 random bytes) suitable for `BETTER_AUTH_SECRET`.
|
|
300
395
|
*/
|
|
301
396
|
generateAuthSecret() {
|
|
302
397
|
return crypto.randomBytes(32).toString("hex");
|
|
@@ -305,26 +400,28 @@ BETTER_AUTH_SECRET=""
|
|
|
305
400
|
* Write the generated auth secret to .env.example
|
|
306
401
|
*/
|
|
307
402
|
async writeAuthSecret() {
|
|
308
|
-
const envPath =
|
|
403
|
+
const envPath = path2.join(this.targetPath, ".env.example");
|
|
309
404
|
const secret = this.generateAuthSecret();
|
|
310
|
-
if (await
|
|
311
|
-
let content = await
|
|
405
|
+
if (await fs4.pathExists(envPath)) {
|
|
406
|
+
let content = await fs4.readFile(envPath, "utf-8");
|
|
312
407
|
content = content.replace(
|
|
313
408
|
/BETTER_AUTH_SECRET=.*/,
|
|
314
409
|
`BETTER_AUTH_SECRET=${secret}`
|
|
315
410
|
);
|
|
316
|
-
await
|
|
411
|
+
await fs4.writeFile(envPath, content);
|
|
317
412
|
}
|
|
318
413
|
}
|
|
319
414
|
/**
|
|
320
415
|
* Configure the Better Auth organizations plugin.
|
|
321
416
|
* When enabled, replaces placeholders with organization import and plugin.
|
|
322
417
|
* When disabled, removes the placeholder lines.
|
|
418
|
+
*
|
|
419
|
+
* @param enabled - Whether the organizations (multi-tenant) plugin should be wired into `convex/auth/plugins/index.ts`.
|
|
323
420
|
*/
|
|
324
421
|
async configureOrganizations(enabled) {
|
|
325
|
-
const pluginsPath =
|
|
326
|
-
if (!await
|
|
327
|
-
let content = await
|
|
422
|
+
const pluginsPath = path2.join(this.targetPath, "convex/auth/plugins/index.ts");
|
|
423
|
+
if (!await fs4.pathExists(pluginsPath)) return;
|
|
424
|
+
let content = await fs4.readFile(pluginsPath, "utf-8");
|
|
328
425
|
if (enabled) {
|
|
329
426
|
content = content.replace(
|
|
330
427
|
"// {{ORGANIZATIONS_IMPORT}}",
|
|
@@ -338,10 +435,12 @@ BETTER_AUTH_SECRET=""
|
|
|
338
435
|
content = content.replace(/.*\/\/ \{\{ORGANIZATIONS_IMPORT\}\}\n?/, "");
|
|
339
436
|
content = content.replace(/.*\/\/ \{\{ORGANIZATIONS_PLUGIN\}\}\n?/, "");
|
|
340
437
|
}
|
|
341
|
-
await
|
|
438
|
+
await fs4.writeFile(pluginsPath, content);
|
|
342
439
|
}
|
|
343
440
|
/**
|
|
344
441
|
* Main orchestration method for project initialization
|
|
442
|
+
*
|
|
443
|
+
* @param options - Resolved project options from CLI flags and prompts, driving every scaffolding step (template copy, overlay, port, auth, README, git, install).
|
|
345
444
|
*/
|
|
346
445
|
async initProject(options) {
|
|
347
446
|
const copySpinner = ora("Copying template files...").start();
|
|
@@ -439,10 +538,20 @@ BETTER_AUTH_SECRET=""
|
|
|
439
538
|
secretSpinner.fail("Failed to generate auth secret");
|
|
440
539
|
throw error;
|
|
441
540
|
}
|
|
442
|
-
if (options.
|
|
541
|
+
if (options.monorepo) {
|
|
542
|
+
const monorepoSpinner = ora("Rewriting dependencies for the host workspace...").start();
|
|
543
|
+
try {
|
|
544
|
+
await this.applyMonorepoRewrite(options);
|
|
545
|
+
monorepoSpinner.succeed("Dependencies rewritten for the host workspace");
|
|
546
|
+
} catch (error) {
|
|
547
|
+
monorepoSpinner.fail("Failed to rewrite dependencies for the host workspace");
|
|
548
|
+
throw error;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (options.initGit && !options.monorepo) {
|
|
443
552
|
await this.initGitRepo();
|
|
444
553
|
}
|
|
445
|
-
if (options.installDependencies) {
|
|
554
|
+
if (options.installDependencies && !options.monorepo) {
|
|
446
555
|
await this.installDependencies();
|
|
447
556
|
await this.lintCode();
|
|
448
557
|
await this.formatCode();
|
|
@@ -454,7 +563,7 @@ BETTER_AUTH_SECRET=""
|
|
|
454
563
|
import { join as join2 } from "path";
|
|
455
564
|
|
|
456
565
|
// src/installers/string-utils.ts
|
|
457
|
-
import
|
|
566
|
+
import fs5 from "fs-extra";
|
|
458
567
|
|
|
459
568
|
// src/installers/providers.ts
|
|
460
569
|
var OAUTH_PROVIDERS = {
|
|
@@ -2057,7 +2166,7 @@ function detectIndentation(line) {
|
|
|
2057
2166
|
return match ? match[1] : "";
|
|
2058
2167
|
}
|
|
2059
2168
|
async function replacePlaceholder(filePath, placeholder, content, options) {
|
|
2060
|
-
const fileContent = await
|
|
2169
|
+
const fileContent = await fs5.readFile(filePath, "utf-8");
|
|
2061
2170
|
if (!fileContent.includes(placeholder)) {
|
|
2062
2171
|
if (options?.graceful) {
|
|
2063
2172
|
console.warn(
|
|
@@ -2071,7 +2180,7 @@ async function replacePlaceholder(filePath, placeholder, content, options) {
|
|
|
2071
2180
|
}
|
|
2072
2181
|
if (options?.inline) {
|
|
2073
2182
|
const updatedContent = fileContent.replace(placeholder, content);
|
|
2074
|
-
await
|
|
2183
|
+
await fs5.writeFile(filePath, updatedContent, "utf-8");
|
|
2075
2184
|
return;
|
|
2076
2185
|
}
|
|
2077
2186
|
const lines = fileContent.split("\n");
|
|
@@ -2093,7 +2202,7 @@ async function replacePlaceholder(filePath, placeholder, content, options) {
|
|
|
2093
2202
|
updatedLines.push(line);
|
|
2094
2203
|
}
|
|
2095
2204
|
}
|
|
2096
|
-
await
|
|
2205
|
+
await fs5.writeFile(filePath, updatedLines.join("\n"), "utf-8");
|
|
2097
2206
|
}
|
|
2098
2207
|
function generateCredentialsValue(enabled) {
|
|
2099
2208
|
return `credentials={${enabled}}`;
|
|
@@ -2226,12 +2335,20 @@ ${sections.join("\n\n---\n\n")}`;
|
|
|
2226
2335
|
|
|
2227
2336
|
// src/installers/nextjs.ts
|
|
2228
2337
|
var VexNextJSInstaller = class extends VexFrameworkInstaller {
|
|
2338
|
+
/**
|
|
2339
|
+
* Framework identifier used for template selection and logging.
|
|
2340
|
+
*
|
|
2341
|
+
* @returns The literal string `"nextjs"`.
|
|
2342
|
+
*/
|
|
2229
2343
|
get frameworkName() {
|
|
2230
2344
|
return "nextjs";
|
|
2231
2345
|
}
|
|
2232
2346
|
/**
|
|
2233
2347
|
* Update OAuth configuration in Convex auth options file
|
|
2234
2348
|
* Target file: convex/auth/options.ts
|
|
2349
|
+
*
|
|
2350
|
+
* @param selectedProviders - IDs of the OAuth providers chosen for the project (e.g. `["google", "github"]`).
|
|
2351
|
+
* @param emailPasswordEnabled - Whether email/password authentication is also enabled.
|
|
2235
2352
|
*/
|
|
2236
2353
|
async updateOAuthConfig(selectedProviders, emailPasswordEnabled) {
|
|
2237
2354
|
const authFilePath = join2(this.targetPath, "convex/auth/options.ts");
|
|
@@ -2254,6 +2371,9 @@ var VexNextJSInstaller = class extends VexFrameworkInstaller {
|
|
|
2254
2371
|
/**
|
|
2255
2372
|
* Update OAuth UI configuration in auth client file
|
|
2256
2373
|
* Target file: src/auth/client.tsx
|
|
2374
|
+
*
|
|
2375
|
+
* @param selectedProviders - IDs of the OAuth providers chosen for the project (e.g. `["google", "github"]`).
|
|
2376
|
+
* @param emailPasswordEnabled - Whether email/password authentication is also enabled.
|
|
2257
2377
|
*/
|
|
2258
2378
|
async updateOAuthUIConfig(selectedProviders, emailPasswordEnabled) {
|
|
2259
2379
|
const providersFilePath = join2(this.targetPath, "src/auth/client.tsx");
|
|
@@ -2273,6 +2393,8 @@ var VexNextJSInstaller = class extends VexFrameworkInstaller {
|
|
|
2273
2393
|
/**
|
|
2274
2394
|
* Update .env.example with OAuth environment variables
|
|
2275
2395
|
* Target file: .env.example
|
|
2396
|
+
*
|
|
2397
|
+
* @param selectedProviders - IDs of the OAuth providers chosen for the project (e.g. `["google", "github"]`).
|
|
2276
2398
|
*/
|
|
2277
2399
|
async updateEnvExample(selectedProviders) {
|
|
2278
2400
|
const envFilePath = join2(this.targetPath, ".env.example");
|
|
@@ -2286,6 +2408,8 @@ var VexNextJSInstaller = class extends VexFrameworkInstaller {
|
|
|
2286
2408
|
/**
|
|
2287
2409
|
* Update README with OAuth provider setup guides
|
|
2288
2410
|
* Target file: README.md
|
|
2411
|
+
*
|
|
2412
|
+
* @param selectedProviders - IDs of the OAuth providers chosen for the project (e.g. `["google", "github"]`).
|
|
2289
2413
|
*/
|
|
2290
2414
|
async updateReadme(selectedProviders) {
|
|
2291
2415
|
const readmeFilePath = join2(this.targetPath, "README.md");
|
|
@@ -2300,6 +2424,8 @@ var VexNextJSInstaller = class extends VexFrameworkInstaller {
|
|
|
2300
2424
|
/**
|
|
2301
2425
|
* Update env.mjs with OAuth provider environment variables
|
|
2302
2426
|
* Target file: src/env.mjs
|
|
2427
|
+
*
|
|
2428
|
+
* @param selectedProviders - IDs of the OAuth providers chosen for the project (e.g. `["google", "github"]`).
|
|
2303
2429
|
*/
|
|
2304
2430
|
async updateEnvTs(selectedProviders) {
|
|
2305
2431
|
const envFilePath = join2(this.targetPath, "src/env.mjs");
|
|
@@ -2331,7 +2457,7 @@ function createInstaller(props) {
|
|
|
2331
2457
|
}
|
|
2332
2458
|
|
|
2333
2459
|
// src/index.ts
|
|
2334
|
-
var program = new Command().name("create-vexcms").description("Scaffold a new VEX CMS project").argument("[project-name]", "Project directory name").option("--bare", "Skip marketing site collections, scaffold empty project").option("--orgs", "Enable multi-tenant organizations").version("0.0.2").parse();
|
|
2460
|
+
var program = new Command().name("create-vexcms").description("Scaffold a new VEX CMS project").argument("[project-name]", "Project directory name").option("--bare", "Skip marketing site collections, scaffold empty project").option("--orgs", "Enable multi-tenant organizations").option("--monorepo", "Scaffold into apps/<name> under the detected pnpm workspace root, rewriting dependencies to workspace/catalog protocols").option("--yes", "Accept every prompt default without rendering (for automation)").version("0.0.2").parse();
|
|
2335
2461
|
var args = program.args;
|
|
2336
2462
|
var opts = program.opts();
|
|
2337
2463
|
async function main() {
|
|
@@ -2339,15 +2465,19 @@ async function main() {
|
|
|
2339
2465
|
console.log(chalk2.bold(" create-vexcms"));
|
|
2340
2466
|
console.log();
|
|
2341
2467
|
const bare = opts.bare ?? false;
|
|
2468
|
+
const yes = opts.yes ?? false;
|
|
2469
|
+
const monorepo = opts.monorepo ?? false;
|
|
2342
2470
|
let inputArg;
|
|
2343
2471
|
if (args[0]) {
|
|
2344
2472
|
inputArg = args[0];
|
|
2473
|
+
} else if (yes) {
|
|
2474
|
+
inputArg = "my-vexcms-app";
|
|
2345
2475
|
} else {
|
|
2346
2476
|
inputArg = await input({
|
|
2347
2477
|
message: "(1/8) What is your project named?",
|
|
2348
2478
|
default: "my-vexcms-app",
|
|
2349
2479
|
validate: (value) => {
|
|
2350
|
-
const name = value.includes("/") ?
|
|
2480
|
+
const name = value.includes("/") ? path3.basename(value) : value;
|
|
2351
2481
|
if (name === ".") return true;
|
|
2352
2482
|
const result = validateProjectName(name);
|
|
2353
2483
|
if (result.valid) return true;
|
|
@@ -2356,52 +2486,80 @@ async function main() {
|
|
|
2356
2486
|
});
|
|
2357
2487
|
}
|
|
2358
2488
|
inputArg = resolveProjectName(inputArg, process.cwd());
|
|
2359
|
-
const projectName = inputArg === "." ?
|
|
2489
|
+
const projectName = inputArg === "." ? path3.basename(process.cwd()) : path3.basename(inputArg);
|
|
2360
2490
|
const validation = validateProjectName(projectName);
|
|
2361
2491
|
if (!validation.valid) {
|
|
2362
2492
|
displayInvalidNameError(projectName, validation.errors);
|
|
2363
2493
|
process.exit(1);
|
|
2364
2494
|
}
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2495
|
+
let targetDir;
|
|
2496
|
+
let workspaceRoot = null;
|
|
2497
|
+
if (monorepo) {
|
|
2498
|
+
workspaceRoot = await findWorkspaceRoot({ cwd: process.cwd() });
|
|
2499
|
+
if (!workspaceRoot) {
|
|
2500
|
+
console.error(chalk2.red("\nError: --monorepo requires a pnpm workspace."));
|
|
2501
|
+
console.error(chalk2.yellow(`No pnpm-workspace.yaml was found walking up from ${process.cwd()}.`));
|
|
2502
|
+
console.error(chalk2.yellow("Run this command from inside a pnpm workspace, or drop --monorepo."));
|
|
2503
|
+
process.exit(1);
|
|
2504
|
+
}
|
|
2505
|
+
targetDir = path3.join(workspaceRoot, "apps", projectName);
|
|
2506
|
+
if (await checkDirectoryExists(targetDir)) {
|
|
2507
|
+
console.error(chalk2.red(`
|
|
2508
|
+
Error: 'apps/${projectName}' already exists under ${workspaceRoot}.`));
|
|
2509
|
+
console.error(chalk2.yellow("--monorepo refuses to scaffold over an existing directory."));
|
|
2369
2510
|
process.exit(1);
|
|
2370
2511
|
}
|
|
2512
|
+
} else {
|
|
2513
|
+
targetDir = inputArg === "." ? process.cwd() : path3.resolve(process.cwd(), inputArg);
|
|
2514
|
+
if (await checkDirectoryExists(targetDir)) {
|
|
2515
|
+
if (!await isDirectoryEmpty(targetDir)) {
|
|
2516
|
+
displayDirectoryNotEmptyError();
|
|
2517
|
+
process.exit(1);
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2371
2520
|
}
|
|
2372
2521
|
let framework;
|
|
2373
|
-
|
|
2374
|
-
framework =
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2522
|
+
if (yes) {
|
|
2523
|
+
framework = "nextjs";
|
|
2524
|
+
} else {
|
|
2525
|
+
while (true) {
|
|
2526
|
+
framework = await select({
|
|
2527
|
+
message: "(2/8) Select a framework:",
|
|
2528
|
+
choices: [
|
|
2529
|
+
{ name: "Next.js (Recommended)", value: "nextjs" },
|
|
2530
|
+
{ name: "TanStack Start (Coming Soon)", value: "tanstack" }
|
|
2531
|
+
]
|
|
2532
|
+
});
|
|
2533
|
+
if (framework === "tanstack") {
|
|
2534
|
+
console.log(chalk2.yellow("\n TanStack Start support is coming soon! Please select Next.js for now.\n"));
|
|
2535
|
+
continue;
|
|
2536
|
+
}
|
|
2537
|
+
break;
|
|
2384
2538
|
}
|
|
2385
|
-
break;
|
|
2386
2539
|
}
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2540
|
+
let port;
|
|
2541
|
+
if (yes) {
|
|
2542
|
+
port = 3010;
|
|
2543
|
+
} else {
|
|
2544
|
+
const portInput = await input({
|
|
2545
|
+
message: "(3/8) Dev server port:",
|
|
2546
|
+
default: "3010",
|
|
2547
|
+
validate: (value) => {
|
|
2548
|
+
const num = parseInt(value, 10);
|
|
2549
|
+
if (isNaN(num) || num < 1 || num > 65535) return "Must be a valid port number (1-65535)";
|
|
2550
|
+
return true;
|
|
2551
|
+
}
|
|
2552
|
+
});
|
|
2553
|
+
port = parseInt(portInput, 10);
|
|
2554
|
+
}
|
|
2555
|
+
const emailPasswordAuth = yes ? true : await confirm({
|
|
2398
2556
|
message: "(4/8) Enable email/password authentication?",
|
|
2399
2557
|
default: true
|
|
2400
2558
|
});
|
|
2401
|
-
const orgs = opts.orgs ?? await confirm({
|
|
2559
|
+
const orgs = opts.orgs ?? (yes ? false : await confirm({
|
|
2402
2560
|
message: "(5/8) Enable multi-tenant (organizations)?",
|
|
2403
2561
|
default: false
|
|
2404
|
-
});
|
|
2562
|
+
}));
|
|
2405
2563
|
const popularProviders = getPopularProviders();
|
|
2406
2564
|
const additionalProviders = getAdditionalProviders();
|
|
2407
2565
|
const allProviderChoices = [
|
|
@@ -2415,15 +2573,15 @@ async function main() {
|
|
|
2415
2573
|
value: p.id
|
|
2416
2574
|
}))
|
|
2417
2575
|
];
|
|
2418
|
-
const oauthProviders = await checkbox({
|
|
2576
|
+
const oauthProviders = yes ? [] : await checkbox({
|
|
2419
2577
|
message: "(6/8) Select OAuth providers (space to toggle, enter to confirm):",
|
|
2420
2578
|
choices: allProviderChoices
|
|
2421
2579
|
});
|
|
2422
|
-
const initGit = await confirm({
|
|
2580
|
+
const initGit = yes ? true : await confirm({
|
|
2423
2581
|
message: "(7/8) Initialize a Git repository?",
|
|
2424
2582
|
default: true
|
|
2425
2583
|
});
|
|
2426
|
-
const installDependencies = await confirm({
|
|
2584
|
+
const installDependencies = yes ? false : await confirm({
|
|
2427
2585
|
message: "(8/8) Install dependencies?",
|
|
2428
2586
|
default: false
|
|
2429
2587
|
});
|
|
@@ -2437,9 +2595,12 @@ async function main() {
|
|
|
2437
2595
|
emailPasswordAuth,
|
|
2438
2596
|
oauthProviders,
|
|
2439
2597
|
initGit,
|
|
2440
|
-
installDependencies
|
|
2598
|
+
installDependencies,
|
|
2599
|
+
monorepo,
|
|
2600
|
+
workspaceRoot,
|
|
2601
|
+
yes
|
|
2441
2602
|
};
|
|
2442
|
-
await
|
|
2603
|
+
await fs6.ensureDir(targetDir);
|
|
2443
2604
|
const installer = createInstaller({
|
|
2444
2605
|
framework,
|
|
2445
2606
|
projectDir: targetDir,
|
|
@@ -2449,6 +2610,10 @@ async function main() {
|
|
|
2449
2610
|
await installer.initProject(options);
|
|
2450
2611
|
console.log();
|
|
2451
2612
|
displaySuccessMessage(options.projectName, targetDir, inputArg === ".");
|
|
2613
|
+
if (monorepo && workspaceRoot) {
|
|
2614
|
+
console.log(chalk2.cyan(` Run 'pnpm install' from ${workspaceRoot} to link the new workspace member.`));
|
|
2615
|
+
console.log();
|
|
2616
|
+
}
|
|
2452
2617
|
}
|
|
2453
2618
|
main().catch((error) => {
|
|
2454
2619
|
console.error(chalk2.red("\nAn error occurred:"), error.message);
|