Skip to content

feat: add create-vinext-app CLI for scaffolding new projects#1032

Open
Divkix wants to merge 22 commits intocloudflare:mainfrom
Divkix:feat/create-vinext-app
Open

feat: add create-vinext-app CLI for scaffolding new projects#1032
Divkix wants to merge 22 commits intocloudflare:mainfrom
Divkix:feat/create-vinext-app

Conversation

@Divkix
Copy link
Copy Markdown
Contributor

@Divkix Divkix commented May 3, 2026

Summary

This PR introduces create-vinext-app, a CLI tool for scaffolding new vinext projects targeting Cloudflare Workers.

What's Included

Core CLI Features

  • Interactive prompts with @clack/prompts for project name and template selection
  • CLI arguments: --yes, --template <app|pages>, --skip-install, --no-git
  • Package manager auto-detection: npm, pnpm, yarn, bun
  • Project name validation: npm naming conventions, reserved names, scoped packages
  • Catalog version baking: Reads pnpm-workspace.yaml to inject real dependency versions

Templates

  • App Router template: RSC, API routes, Cloudflare Workers configuration
  • Pages Router template: Full middleware/routing/SSR, API routes, Link components

Testing & CI

  • 59 passing tests (unit + integration)
  • New CI job create-vinext-app tests both templates
  • Publish workflow job for automated releases

Usage

# Interactive
npm create vinext-app

# Quick start with defaults
npm create vinext-app my-app --yes

# Choose template
npm create vinext-app my-app --template pages

Files Added

packages/create-vinext-app/
├── src/
│   ├── index.ts          # CLI entry with arg parsing
│   ├── install.ts        # Package manager detection
│   ├── validate.ts       # Project name validation
│   ├── catalog.ts        # Version baking from workspace
│   ├── scaffold.ts       # Template copying
│   └── prompts.ts        # Interactive prompts
├── templates/
│   ├── app-router/       # 10 template files
│   └── pages-router/     # 9 template files
├── test/
│   ├── cli.test.ts       # 39 tests
│   ├── scaffold.test.ts  # 10 tests
│   └── integration.test.ts # 10 tests
└── README.md

Checklist

  • All 59 tests passing
  • Build successful
  • Manual CLI testing verified
  • CI workflow updated
  • Publish workflow added
  • README documentation added
  • Root README updated

Closes the gap for users wanting to start fresh vinext projects without migrating from Next.js.

Ref: #407

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 3, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1032

commit: ae20b22

Divkix added 4 commits May 2, 2026 18:58
Remove the fragile 'vp add -g' global install with tgz glob pattern
that fails with 'Not a directory (os error 20)'. Run the built CLI
directly via node instead of requiring a global install.

Fix formatting across template files and add @ts-nocheck to template
files that reference modules unavailable in the monorepo.
Use 'vp pack' instead of 'vp run build' to ensure the package-local
build script runs instead of the root workspace build. Fix scaffold
step working-directory so 'node dist/index.js' resolves correctly.

Add create-vinext-app workspace config to knip with proper entry
points, template file ignores, and tsx binary ignore. Re-export
unused types from index.ts so knip recognizes them as consumed.
vp pack outputs .mjs files but package.json bin and main fields
pointed to dist/index.js which doesn't exist. Update both to
dist/index.mjs and fix the CI scaffold step to match.
…xt-app

Remove auto-detected entry and redundant ignoreFiles/ignoreWorkspaces
entries. Add vitest to per-workspace ignoreDependencies since it is
imported through vite-plus/test and invisible to knip.
@southpolesteve
Copy link
Copy Markdown
Collaborator

/bigbonk review this PR please

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: create-vinext-app CLI

The overall structure is solid — good separation of concerns across install.ts, validate.ts, catalog.ts, scaffold.ts, prompts.ts, and index.ts. Tests are well-organized. A few issues need addressing before merge:

Issues

  1. Dead code: worker/index.ts in app-router template is unused. The app-router wrangler.jsonc.tmpl sets "main": "vinext/server/app-router-entry", meaning wrangler never loads worker/index.ts. Either remove the file, or change wrangler.jsonc to point at "main": "./worker/index.ts" (like the existing examples/app-router-cloudflare/ does). The latter is better since it gives users a visible entry point they can extend (e.g., image optimization).

  2. globals.css is never imported. The app-router template ships app/globals.css but app/layout.tsx never imports it, so it has zero effect. Either add the import or remove the file.

  3. Duplicated install command logic. install.ts exports buildInstallCommand() and scaffold.ts has a private getInstallCommand() that does the same thing with a slightly different return shape. scaffold.ts never imports from install.ts. Use one function.

  4. catalog.ts version resolution is fragile at publish time. The comment says versions are baked "at build time" but getTemplateVersions() actually runs at scaffold time — it reads pnpm-workspace.yaml relative to import.meta.url. When the published package runs from a user's global npm cache, the monorepo file won't exist, so getTemplateVersions() returns {}. This means all template variables like {{REACT_VERSION}} will be left as-is in the scaffolded output. The processTmplFiles function only substitutes keys present in the vars map — absent keys remain literal {{...}} strings in package.json. This is a critical bug for published usage.

  5. Pages Router worker template is 246 lines of complex production server logic. This is a full reimplementation of middleware, redirects, rewrites, basePath handling, etc. It will immediately fall out of sync with packages/vinext/src/server/prod-server.ts. When bugs get fixed in the main server, they won't be reflected here. This should import from vinext instead of duplicating logic.

  6. The plan/spec docs shouldn't be committed. docs/superpowers/plans/ and docs/superpowers/specs/ add 2,575 lines of AI-generated implementation plan that will never be maintained. These are implementation artifacts, not user-facing docs.

Minor issues

  1. package.json lists "vitest": "catalog:" in devDependencies — per AGENTS.md, vitest should not be installed directly when using vite-plus.

  2. The parseArgs tests monkey-patch process.exit without type safety (process.exit = (code?: number) => { throw ... }). Consider extracting the error-and-exit logic behind a seam instead.

  3. layout.tsx template has {{PROJECT_NAME}} in the metadata title — this works but means a name like @org/my-app becomes the page title, which is awkward for scoped packages.

  4. Missing assets config in wrangler templates — the existing examples include "assets": { "not_found_handling": "none" } which is important for static file serving to work correctly.

"name": "{{WORKER_NAME}}",
"compatibility_date": "2026-05-02",
"compatibility_flags": ["nodejs_compat"],
"main": "vinext/server/app-router-entry",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This points at vinext/server/app-router-entry but the template also ships worker/index.ts which is never referenced. The existing examples/app-router-cloudflare/ uses "main": "./worker/index.ts" instead.

Either:

  • Remove templates/app-router/worker/index.ts entirely (simplest, and vinext/server/app-router-entry is the documented simple path), or
  • Change this to "./worker/index.ts" to match the established pattern and give users an extensible entry point
Suggested change
"main": "vinext/server/app-router-entry",
"main": "./worker/index.ts",

@@ -0,0 +1,14 @@
import type { ReactNode } from "react";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

app/globals.css exists in the template but is never imported here — it won't be loaded.

Suggested change
import type { ReactNode } from "react";
import type { ReactNode } from "react";
import "./globals.css";

Comment on lines +107 to +112
const installCmd = getInstallCommand(options.pm);
exec(installCmd.cmd, installCmd.args, { cwd: projectPath });
}
}

function getInstallCommand(pm: "npm" | "pnpm" | "yarn" | "bun"): { cmd: string; args: string[] } {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duplicates buildInstallCommand() from install.ts with a slightly different return shape ({ cmd, args } vs string[]). scaffold.ts doesn't import install.ts at all. Use buildInstallCommand from install.ts instead:

Suggested change
const installCmd = getInstallCommand(options.pm);
exec(installCmd.cmd, installCmd.args, { cwd: projectPath });
}
}
function getInstallCommand(pm: "npm" | "pnpm" | "yarn" | "bun"): { cmd: string; args: string[] } {
const [cmd, ...args] = buildInstallCommand(options.pm);
exec(cmd, args, { cwd: projectPath });
}
}

And add import { buildInstallCommand } from "./install.js"; at the top, removing the private getInstallCommand function.

try {
const __dirname = dirname(fileURLToPath(import.meta.url));
// Walk up from dist/ (packages/create-vinext-app/dist/) to monorepo root
const workspaceYaml = resolve(__dirname, "..", "..", "..", "pnpm-workspace.yaml");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The design doc says versions are "baked at build time" but this runs at scaffold time. When the published package is installed globally by a user, pnpm-workspace.yaml won't exist 3 directories up from the installed dist/ folder, so getTemplateVersions() returns {}.

With empty version vars, processTmplFiles will leave {{REACT_VERSION}}, {{VITE_VERSION}}, etc. as literal strings in the scaffolded package.json. The user gets a broken project.

This needs to either:

  1. Actually bake versions at build time (e.g., a build script that writes a versions.json into dist/), or
  2. Have scaffold.ts check for un-substituted {{...}} vars and error, or
  3. Have fallback versions hardcoded in catalog.ts when the workspace file isn't found

@@ -0,0 +1,246 @@
// @ts-nocheck -- template file, modules resolved in scaffolded project
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is 246 lines duplicating the production server logic from packages/vinext/src/server/prod-server.ts — middleware dispatch, redirect/rewrite matching, basePath handling, trailing slash normalization, config header merging, etc.

Per AGENTS.md: "When fixing a bug in any of these files, check whether the same bug exists in the others. Do not leave known bugs as 'follow-ups' — fix them in the same PR."

This template will immediately diverge from the main server. Every bug fix in prod-server.ts would need to be manually ported here. The app-router template avoids this by delegating to vinext/server/app-router-entry. The pages-router template should do something similar — either:

  • Point wrangler.jsonc main at the vinext pages router entry directly, or
  • Import and delegate to vinext/server/prod-server or similar, or
  • At minimum, add a comment explaining why this duplication is intentional and how to keep it in sync

"preview": "vp preview"
},
"dependencies": {
"vinext": "^{{VINEXT_VERSION}}",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same double-caret issue as app-router template — index.ts already adds ^ prefix to the version.

Suggested change
"vinext": "^{{VINEXT_VERSION}}",
"vinext": "{{VINEXT_VERSION}}",

@@ -0,0 +1,2262 @@
# create-vinext-app Implementation Plan
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This 2,262-line implementation plan is an AI scaffolding artifact, not documentation. It won't be maintained and adds noise to the repo. Remove this file from the PR.

@@ -0,0 +1,313 @@
# Spec: create-vinext-app Scaffolding CLI
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the plan doc — this is an implementation artifact, not user-facing documentation. Remove from the PR.

Comment thread packages/create-vinext-app/src/index.ts Outdated
install: !opts.skipInstall,
git: !opts.noGit,
pm,
vinextVersion: `^${VERSION}`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a ^ prefix to the version, but the template package.json.tmpl already has "vinext": "^{{VINEXT_VERSION}}". The result is "vinext": "^^0.0.5". Fix this here or in the template (see template comments).

Also: the catalog.ts getTemplateVersions() already populates {{VINEXT_VERSION}} with the raw version from package.json. So there are two competing sources setting {{VINEXT_VERSION}}catalog.ts (line 50) and the scaffold() call here which passes vinextVersion separately. The scaffold.ts processTmplFiles call puts {{VINEXT_VERSION}}: vinextVersion after the spread of versionVars, so it overwrites the catalog version. This works but is confusing. Consider having one canonical source for the vinext version.

Comment on lines +207 to +209
test("exits on invalid template", () => {
const prevExit = process.exit;
process.exit = (code?: number) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Monkey-patching process.exit is fragile (TypeScript complains, teardown issues on test failure). Consider refactoring parseArgs to throw or return an error result instead of calling process.exit directly — this would make it both more testable and safer to use as a library.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 3, 2026

Review posted. Here's a summary of the key findings:

Blocking issues

  1. Double-caret version bugpackage.json.tmpl has "vinext": "^{{VINEXT_VERSION}}" and index.ts passes vinextVersion: "^${VERSION}", producing "vinext": "^^0.0.5" in scaffolded output.

  2. catalog.ts doesn't actually bake versions at build time — it reads pnpm-workspace.yaml at scaffold time via import.meta.url. When the published CLI runs from a user's npm cache, the file won't exist, getTemplateVersions() returns {}, and all {{REACT_VERSION}}/{{VITE_VERSION}}/etc. placeholders remain as literal strings in the scaffolded package.json.

  3. Dead worker/index.ts in app-router template — wrangler config points at vinext/server/app-router-entry, making the worker file unused.

  4. globals.css never imported — file exists but layout.tsx never imports it.

  5. 246-line pages-router worker duplicates prod server logic — will immediately diverge from packages/vinext/src/server/prod-server.ts.

Minor issues

  • Duplicated getInstallCommand / buildInstallCommand logic
  • vitest shouldn't be a direct dependency when using vite-plus
  • AI plan/spec docs (2,575 lines) committed as permanent files
  • Missing assets config in wrangler templates
  • process.exit monkey-patching in tests

github run

Divkix added 3 commits May 2, 2026 20:05
…dup, template fixes

- Fix double-caret version bug (remove ^ from code, keep in template)
- Bake catalog versions at build time via scripts/bake-versions.mjs
- Create vinext/server/pages-router-entry, simplify pages-router worker
- Point wrangler.jsonc at ./worker/index.ts, add assets config
- Import globals.css in app layout template
- Deduplicate install command logic
- Remove vitest as direct dependency
- Use DISPLAY_NAME for scoped package metadata titles
- Refactor parseArgs to return errors instead of calling process.exit
- Update tests for new worker delegation and error handling
@Divkix
Copy link
Copy Markdown
Contributor Author

Divkix commented May 3, 2026

Heads up!
The scaffolding here leans on @cloudflare/vite-plugin and wrangler right now because Workers is the smoothest deployment path we've built first. I'll follow up with a PR that makes the templates platform-agnostic (Nitro, standalone Node, etc.) so create-vinext-app matches what the main README says about deploying anywhere. This is just the starting point to get something working end-to-end.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Unknown error
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@NathanDrake2406
Copy link
Copy Markdown
Contributor

@codex review please

@NathanDrake2406
Copy link
Copy Markdown
Contributor

let's see if it works this time, great PR bro!

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 126abadcd6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/create-vinext-app/src/index.ts Outdated
Comment on lines +152 to +153
projectNameStr.includes(path.sep) ||
projectNameStr.includes("/");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve scoped names instead of treating them as paths

The path detection treats any project name containing / as a filesystem path, so a valid scoped package like @org/my-app is rewritten into a nested directory path and loses its scope in package.json (it becomes just my-app). This conflicts with the scoped-name validation logic and breaks expected CLI behavior for users who intentionally provide npm-scoped names.

Useful? React with 👍 / 👎.


export default {
async fetch(request: Request): Promise<Response> {
return handler.fetch(request);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Forward env and ctx to the app-router worker handler

The scaffolded App Router worker calls handler.fetch(request) without passing Cloudflare env and ctx. That prevents the entry from accessing env.ASSETS for static-file resolution and from using request execution context for background work, so scaffolded apps can return the internal static-file signal response instead of actual public/ assets and lose waitUntil-backed behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@james-elicx GPT's ability to catch bugs are so superior...
anomalyco/opencode#24431 is fixed so I think we can update to new opencode now without getting ProviderInIt.

anomalyco/opencode#24432 is just getting assigned tho, I'll let u know when they fixed it. Do u want to try 5.5 one more time?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, Fred's new agents framework look like there's a lot more room for us to customise

const middlewareHeaders: Record<string, string | string[]> = {};
let middlewareRewriteStatus: number | undefined;
if (typeof runMiddleware === "function") {
const result = await runMiddleware(request);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass a basePath-stripped request into middleware

After stripping pathname for routing, middleware is still executed with the original request.url. In basePath deployments, middleware matcher evaluation runs against the unstripped path (for example /docs/about instead of /about), causing matcher-based middleware to be skipped in Workers while the Node production path strips before middleware.

Useful? React with 👍 / 👎.

…eware

- Exclude scoped npm package names (@org/pkg) from path detection in
  create-vinext-app, preserving the scope prefix in package.json.
- Forward env and ctx to handler.fetch() in both app-router and
  pages-router worker templates so static file resolution and
  ctx.waitUntil() work in scaffolded apps.
- Strip basePath from the request URL before passing it to middleware
  in pages-router-entry.ts. Without this, middleware matchers evaluate
  against basePath-prefixed paths (e.g. /docs/about instead of /about)
  and silently skip middleware. deploy.ts and prod-server.ts already
  handle this correctly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants