feat: add create-vinext-app scaffolding CLI#406
feat: add create-vinext-app scaffolding CLI#406Divkix wants to merge 10 commits intocloudflare:mainfrom
Conversation
Adds `packages/create-vinext-app/` — a standalone CLI for `npm create vinext-app@latest` that scaffolds new vinext projects targeting Cloudflare Workers with a single command. - Two templates: App Router (with RSC) and Pages Router - Interactive prompts via @clack/prompts, --yes for CI usage - Package manager auto-detection from npm_config_user_agent - .tmpl variable substitution for project/worker names - 101 tests across 6 files (unit, integration, e2e) - CI job to verify scaffolded projects start (ubuntu + windows) - Publish workflow updated to release alongside vinext - README, AGENTS.md, CONTRIBUTING.md, migration skill updated - Root build/lint/format configs updated to include new package
commit: |
…dex.ts in app-router template The app-router template's wrangler.jsonc.tmpl referenced ./worker/index.ts, but no such file exists in the template. Only pages-router has a custom worker entry. App-router should use vinext/server/app-router-entry directly, as documented in examples/app-router-cloudflare/worker/index.ts. Adds a test to prevent regression.
CI on Windows passes `D:\a\_temp/cva-test` as the project name arg. The validation regex rejects backslashes and colons. When the input is an absolute path, extract path.basename() for the project name and use the full path for the target directory.
There was a problem hiding this comment.
Pull request overview
Adds a new standalone scaffolding CLI (create-vinext-app) to generate ready-to-run vinext + Cloudflare Workers projects via npm create vinext-app@latest, plus supporting templates, tests, CI, and docs/workflows updates.
Changes:
- Introduces
packages/create-vinext-app/CLI with prompts, validation, install detection, and template-based scaffolding. - Adds App Router + Pages Router templates and a comprehensive Vitest suite (unit/integration/e2e).
- Updates monorepo build, CI, publish workflow, and documentation to include the new CLI.
Reviewed changes
Copilot reviewed 42 out of 45 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.json | Excludes scaffold templates from TS compilation. |
| pnpm-lock.yaml | Adds create-vinext-app and @clack/prompts deps. |
| packages/create-vinext-app/vitest.config.ts | Vitest config for the new package. |
| packages/create-vinext-app/tsconfig.json | TS build settings for the CLI package. |
| packages/create-vinext-app/tests/validate.test.ts | Unit tests for name/path/empty-dir validation. |
| packages/create-vinext-app/tests/templates.test.ts | Verifies template file presence and contents. |
| packages/create-vinext-app/tests/scaffold.test.ts | Integration tests for scaffolding behavior. |
| packages/create-vinext-app/tests/install.test.ts | Tests package-manager detection and install commands. |
| packages/create-vinext-app/tests/helpers.ts | Shared test helpers and exec stub. |
| packages/create-vinext-app/tests/e2e.test.ts | End-to-end CLI tests (help/version/scaffold). |
| packages/create-vinext-app/tests/cli.test.ts | CLI arg parsing + main flow tests with mocks. |
| packages/create-vinext-app/templates/pages-router/wrangler.jsonc.tmpl | Pages Router Wrangler template. |
| packages/create-vinext-app/templates/pages-router/worker/index.ts | Pages Router Worker entry template. |
| packages/create-vinext-app/templates/pages-router/vite.config.ts | Pages Router Vite config template. |
| packages/create-vinext-app/templates/pages-router/tsconfig.json | Pages Router TS config template. |
| packages/create-vinext-app/templates/pages-router/pages/index.tsx | Pages Router sample home page. |
| packages/create-vinext-app/templates/pages-router/pages/api/hello.ts | Pages Router sample API route. |
| packages/create-vinext-app/templates/pages-router/pages/about.tsx | Pages Router sample about page. |
| packages/create-vinext-app/templates/pages-router/package.json.tmpl | Pages Router package.json template. |
| packages/create-vinext-app/templates/pages-router/next-shims.d.ts | Type shims for Next-like imports. |
| packages/create-vinext-app/templates/pages-router/_gitignore | Gitignore template (renamed on scaffold). |
| packages/create-vinext-app/templates/app-router/wrangler.jsonc.tmpl | App Router Wrangler template. |
| packages/create-vinext-app/templates/app-router/vite.config.ts | App Router Vite config template. |
| packages/create-vinext-app/templates/app-router/tsconfig.json | App Router TS config template. |
| packages/create-vinext-app/templates/app-router/package.json.tmpl | App Router package.json template. |
| packages/create-vinext-app/templates/app-router/app/page.tsx | App Router sample page. |
| packages/create-vinext-app/templates/app-router/app/layout.tsx | App Router sample layout. |
| packages/create-vinext-app/templates/app-router/app/api/hello/route.ts | App Router sample route handler. |
| packages/create-vinext-app/templates/app-router/_gitignore | Gitignore template (renamed on scaffold). |
| packages/create-vinext-app/src/validate.ts | Project name/path and directory validation. |
| packages/create-vinext-app/src/scaffold.ts | Copies templates, substitutes vars, runs git/install. |
| packages/create-vinext-app/src/prompts.ts | Interactive prompting via @clack/prompts. |
| packages/create-vinext-app/src/install.ts | Package manager detection + install command builder. |
| packages/create-vinext-app/src/index.ts | CLI entrypoint + arg parsing + orchestration. |
| packages/create-vinext-app/package.json | New package manifest and scripts. |
| package.json | Includes create-vinext-app in build pipeline. |
| README.md | Documents npm create vinext-app@latest. |
| CONTRIBUTING.md | Adds contributor notes for the new package. |
| AGENTS.md | Updates structure/testing guidance for new package. |
| .oxlintrc.json | Ignores scaffold templates for linting. |
| .oxfmtrc.json | Ignores scaffold templates for formatting. |
| .github/workflows/publish.yml | Publishes create-vinext-app alongside vinext. |
| .github/workflows/ci.yml | Adds matrix job to scaffold + boot a fresh app. |
| .agents/skills/migrate-to-vinext/references/compatibility.md | Notes new scaffolding flow replaces create-next-app. |
| .agents/skills/migrate-to-vinext/SKILL.md | Adds “Starting a new project?” guidance. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ronments Pages Router projects on Cloudflare Workers failed with "module is not defined" because react/jsx-runtime (CJS) was discovered lazily by Vite's dep optimizer. The workerd module runner imported the raw CJS source before the optimizer could convert it to ESM. Add a configEnvironment hook that pre-includes React packages in optimizeDeps.include for all non-client Cloudflare environments, ensuring they are pre-bundled before the module runner needs them.
…matrix CI only tested the app-router template in the scaffold → install → dev server → HTTP 200 integration flow. This allowed a pages-router bug (ReferenceError: module is not defined) to slip through undetected. Add `template: [app, pages]` to the matrix so both templates get the full integration test on both OS targets (2×2 = 4 jobs).
- Derive project name from cwd basename when "." is passed, preventing
empty string in package.json/wrangler.jsonc from sanitizeWorkerName(".")
- Remove dead no-op ternary in validateProjectName regex check
- Fix misleading "lowercase letters" error message (uppercase is accepted)
- Add pretest build step so tests work from clean checkout
|
Will do some manual passes + testing over the diff later as well. /bigbonk review |
There was a problem hiding this comment.
Review: create-vinext-app scaffolding CLI
Solid foundation — the architecture is clean, the test coverage is thorough (102 tests across 6 files), and the templates are well-structured. The CI integration and publish workflow updates are thoughtful. I have a few issues that should be addressed before merging, one of which is a correctness bug that will bite CI and path-based invocations.
Summary of issues
-
Bug (blocking):
normalizedNameis used as both the package name and filesystem path, which breaks when a path (absolute or relative with separators) is passed as the positional arg. The CI workflow itself triggers this:node ... "${{ runner.temp }}/cva-test"— thepackage.jsonname field will get set tocva-testwhich happens to be fine for basename, but thetargetDirextraction only handlespath.isAbsolute(), not relative paths like../foo/bar. -
Bug:
prompts.tsonCancelcallsprocess.exit(0)despite the JSDoc claiming it returns null. Makes the function untestable for cancellation and contradictsmain()'sif (!answers) returnguard. -
Correctness:
validate.tsregex allows uppercase but the function then normalizes — this works, but the error message for the character class check says "letters" (ambiguous). More importantly, scoped names like@org/pass the regex butbareNameextraction with.split("/").pop()could return empty string for@org/(trailing slash, no package name). -
DX:
stdio: "pipe"ongit initandnpm installhides all output including errors from the user. -
Template: The pages-router
worker/index.tsis 242 lines of production-grade request handling code duplicated from the vinext core. This will drift. -
Unrelated change: The
configEnvironmenthook addition inpackages/vinext/src/index.tsshould be in a separate PR.
Details inline.
| if (path.isAbsolute(projectName)) { | ||
| targetDir = projectName; | ||
| projectName = path.basename(projectName); | ||
| } |
There was a problem hiding this comment.
Bug: relative paths with directory separators are not handled.
This only handles path.isAbsolute(), but users can pass relative paths like ../my-app or ./projects/my-app. In those cases, projectName stays as ../my-app, which then fails validateProjectName (. and / are valid in the regex but .. isn't a sensible package name).
Also, even for the absolute path case: after projectName = path.basename(projectName), the resulting name is used in both validateProjectName and as the final normalizedName. If the basename itself has uppercase (e.g. /tmp/My-App), validation returns normalized: "my-app", but targetDir is still /tmp/My-App. So the directory is created at the uppercase path but package.json gets the lowercase name — which is correct behavior, but worth a test.
Consider handling relative paths with separators too:
| } | |
| let targetDir: string | undefined; | |
| if (path.isAbsolute(projectName)) { | |
| targetDir = projectName; | |
| projectName = path.basename(projectName); | |
| } else if (projectName.includes(path.sep) || projectName.includes("/")) { | |
| targetDir = path.resolve(process.cwd(), projectName); | |
| projectName = path.basename(projectName); | |
| } |
| { | ||
| onCancel: () => { | ||
| p.cancel("Cancelled."); | ||
| process.exit(0); |
There was a problem hiding this comment.
Bug: process.exit(0) contradicts the documented return type.
The JSDoc says "Returns null if the user cancels (Ctrl+C)" and main() has if (!answers) return to handle this. But onCancel calls process.exit(0), so main() never gets the chance to handle cancellation gracefully.
This also makes the cancellation path untestable without mocking process.exit.
| process.exit(0); | |
| onCancel: () => { | |
| p.cancel("Cancelled."); | |
| return; | |
| }, |
Then after p.group(), check if the result contains a Symbol (which @clack/prompts returns on cancel) and return null:
if (p.isCancel(answers.projectName) || p.isCancel(answers.template)) {
p.cancel("Cancelled.");
return null;
}| } | ||
|
|
||
| function defaultExec(cmd: string, args: string[], opts: { cwd: string }): void { | ||
| execFileSync(cmd, args, { cwd: opts.cwd, stdio: "pipe" }); |
There was a problem hiding this comment.
DX issue: stdio: "pipe" silently swallows all subprocess output.
When npm install fails (network error, resolution conflict, etc.), the user sees nothing — just a thrown error with no diagnostic output. Same for git init failures.
For a scaffolding CLI, stdio: "inherit" is the standard approach — it lets the user see install progress and any error messages.
| execFileSync(cmd, args, { cwd: opts.cwd, stdio: "pipe" }); | |
| function defaultExec(cmd: string, args: string[], opts: { cwd: string }): void { | |
| execFileSync(cmd, args, { cwd: opts.cwd, stdio: "inherit" }); | |
| } |
| }; | ||
| } | ||
|
|
||
| if (!/^[a-zA-Z0-9\-._@/]+$/.test(name)) { |
There was a problem hiding this comment.
Edge case: scoped names with missing package part.
The regex ^[a-zA-Z0-9\-._@/]+$ accepts @org/ (with trailing slash but no package name). Then bareName = name.split("/").pop() returns "", which passes the ^\d and startsWith("-") checks vacuously. The result is that @org/ is accepted as valid.
Consider adding a check that scoped names actually have a non-empty part after the slash:
if (name.startsWith("@") && !name.match(/^@[a-z0-9-]+\/[a-z0-9][a-z0-9._-]*$/i)) {
return { valid: false, message: "Scoped package name must be in the format @scope/name" };
}| @@ -0,0 +1,242 @@ | |||
| /** | |||
There was a problem hiding this comment.
Maintainability concern: 242 lines of request handling code duplicated from vinext core.
This is a copy of packages/vinext/src/server/prod-server.ts (or very close to it). As noted in 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.
A scaffolded template that ships a full copy of the production server handler will immediately drift from the core. When bugs are fixed in prod-server.ts, this template won't get the fix.
Consider whether the pages-router template could use a vinext-provided entry point (like the app-router template uses vinext/server/app-router-entry) instead of inlining the full handler. If a pages-router equivalent doesn't exist yet, it might be worth creating one — even a thin wrapper — so templates don't carry a full copy of server logic.
| if (entryFile) { | ||
| const resolvedEntry = path.resolve(entryFile); | ||
| // Handle both .ts (dev) and .js (built) entry | ||
| if (resolvedEntry === thisFile || resolvedEntry === thisFile.replace(/\.ts$/, ".js")) { |
There was a problem hiding this comment.
Fragile entry point detection.
This comparison relies on exact path matching between import.meta.url and process.argv[1]. It handles .ts → .js but not symlinks (common with npm link or global installs), nor does it handle Windows path casing differences.
A simpler and more robust pattern:
const isDirectRun = process.argv[1] &&
path.resolve(process.argv[1]).replace(/\.ts$/, ".js") === thisFile.replace(/\.ts$/, ".js");Or just use the bin field — if the file is invoked via create-vinext-app, process.argv[1] will be a symlink to dist/index.js. Consider using fs.realpathSync on both sides.
| "vitest": "^3.2.1" | ||
| }, | ||
| "engines": { | ||
| "node": ">=22" |
There was a problem hiding this comment.
node >= 22 is a high minimum.
Node 18 is still in maintenance LTS (until April 2025) and Node 20 is the current LTS. Requiring Node 22 will exclude a significant portion of users. Unless there's a specific Node 22 API being used (I don't see one in the source), consider lowering to >=18 or >=20.
| working-directory: packages/vinext | ||
|
|
||
| - name: Scaffold a fresh create-vinext-app project | ||
| run: node packages/create-vinext-app/dist/index.js "${{ runner.temp }}/cva-test" --template ${{ matrix.template }} --yes --skip-install |
There was a problem hiding this comment.
CI passes an absolute path as the positional arg.
This feeds ${{ runner.temp }}/cva-test as the project name. The code at index.ts:140 correctly handles path.isAbsolute() by extracting the basename. However, the resulting package.json will have "name": "cva-test" and the worker name will be "cva-test". That's fine and works, but it's worth noting that this CI step is exercising the absolute-path code path, not the typical user flow of create-vinext-app my-app.
Consider adding a second test that uses a plain name (the typical user flow) to ensure both paths are covered in CI.
packages/vinext/src/index.ts
Outdated
| // Pre-include React CJS packages in every non-client Cloudflare | ||
| // environment so Vite's dep optimizer pre-bundles them before the | ||
| // workerd module runner tries to evaluate raw CJS. Without this, | ||
| // react/jsx-runtime is discovered lazily and the runner imports | ||
| // the CJS source directly, failing with "module is not defined". | ||
| configEnvironment(_name, env) { | ||
| if (!hasCloudflarePlugin) return; | ||
| // Skip the client environment — it runs in the browser, not workerd. | ||
| if (env.consumer === "client") return; | ||
|
|
||
| const reactDeps = [ | ||
| "react", | ||
| "react-dom", | ||
| "react-dom/server.edge", | ||
| "react/jsx-runtime", | ||
| "react/jsx-dev-runtime", | ||
| ]; | ||
|
|
||
| env.optimizeDeps ??= {}; | ||
| env.optimizeDeps.include = [ | ||
| ...(env.optimizeDeps.include ?? []), | ||
| ...reactDeps.filter((d) => !(env.optimizeDeps!.include ?? []).includes(d)), | ||
| ]; | ||
| }, | ||
|
|
||
| resolveId: { | ||
| // Hook filter: only invoke JS for next/* imports and virtual:vinext-* modules. | ||
| // Matches "next/navigation", "next/router.js", "virtual:vinext-rsc-entry", |
There was a problem hiding this comment.
Unrelated change: this configEnvironment hook is not related to create-vinext-app.
This is a React CJS pre-bundling fix for Cloudflare environments. It should be in a separate PR with its own test coverage, not bundled with the scaffolding CLI feature.
| "react": "^19.2.4", | ||
| "react-dom": "^19.2.4", | ||
| "vite": "^7.3.1", | ||
| "vinext": "latest", |
There was a problem hiding this comment.
Pinning to "vinext": "latest" is risky for reproducible builds.
Users who scaffold today and install tomorrow might get a different version. Consider using a caret-pinned version that gets updated when the template is updated, or at minimum "vinext": "^0.x.y" with the current version substituted at scaffold time (you already have the .tmpl machinery for this).
This also applies to the pages-router template.
|
Review submitted as "Changes Requested" with 10 inline comments covering:
|
- Handle relative paths (./my-app, ../my-app) in project name resolution - Remove process.exit(0) from cancel handler; use p.isCancel() instead - Change stdio from "pipe" to "inherit" for subprocess output visibility - Reject malformed scoped package names (@org/, @/name, @, @org/foo/bar) - Add TODO comment for pages-router worker entry extraction - Use realpathSync for symlink-safe auto-run detection - Lower Node engine requirement from >=22 to >=18 - Add CI step to smoke-test plain project name scaffolding - Revert unrelated configEnvironment change from vinext core - Document "latest" versioning decision for pre-1.0 templates
…ironments The configEnvironment hook was accidentally reverted in e3d3255. Without it, React's CJS jsx-runtime.js hits workerd's ESM-only module runner and throws "ReferenceError: module is not defined", breaking pages-router template CI.
|
Tests pass and addressed all review issues |
|
/bigbonk review |
There was a problem hiding this comment.
Review: create-vinext-app scaffolding CLI
Good work addressing the issues from the previous review round. The relative path handling (./, ../), process.exit(0) in prompts, stdio: "inherit", scoped name validation, realpathSync for entry detection, Node >=18, and the CI plain-name test are all fixed properly.
A few remaining items:
Still outstanding from previous review
-
configEnvironmenthook is unrelated to this PR — This is a React CJS pre-bundling fix for Cloudflare server environments. It should be split into its own PR with a test demonstrating the problem it solves. Bundling it here makes the diff harder to review and the commit history less useful. -
"vinext": "latest"in templates — The comment inscaffold.ts:90says "Pin to caret range once vinext reaches 1.0" butlatestis problematic even pre-1.0. A user scaffolds today, installs a week later, and gets a different (possibly breaking) version. Since this is a scaffolding CLI with.tmplmachinery already in place, consider substituting the current version at scaffold time (e.g."vinext": "^{{VINEXT_VERSION}}"). -
Pages-router
worker/index.tsduplication — The TODO comment acknowledges this but it's 245 lines of production server logic that will drift from core. This is acceptable as a v1 trade-off, but creatingvinext/server/pages-router-entryshould be tracked as a follow-up issue.
New issues
See inline comments.
| projectName.startsWith("./") || | ||
| projectName.startsWith("../") || | ||
| projectName.startsWith(".\\") || | ||
| projectName.startsWith("..\\"); |
There was a problem hiding this comment.
Bug: bare relative paths without ./ prefix are not detected.
If a user runs create-vinext-app projects/my-app (no ./ prefix), looksLikePath is false because the string doesn't start with ./, ../, or be absolute. The name projects/my-app then goes to validateProjectName() which rejects it (since / in a non-scoped name fails the scoped validation), so the user gets a confusing "Invalid project name" error instead of it being treated as a relative path.
This matters because subdir/name is a natural way to specify a path.
| projectName.startsWith("..\\"); | |
| const looksLikePath = | |
| path.isAbsolute(projectName) || | |
| projectName.startsWith("./") || | |
| projectName.startsWith("../") || | |
| projectName.startsWith(".\\") || | |
| projectName.startsWith("..\\") || | |
| projectName.includes(path.sep) || | |
| projectName.includes("/"); |
With this change, any input containing a separator is treated as a path and resolved accordingly.
| } | ||
|
|
||
| // Use normalized name if available | ||
| const normalizedName = |
There was a problem hiding this comment.
Nit: validation.normalized could be stale after useCwd override.
When "." is passed, validateProjectName(".") returns { valid: true, useCwd: true } with no normalized field. Lines 160-162 then set projectName = path.basename(process.cwd()). But then line 167 checks validation.normalized, which is undefined, so it falls through to projectName — which is correct.
However, if the cwd basename is uppercase (e.g. /home/user/My-Project), projectName becomes My-Project and no normalization happens. The package.json will have an uppercase name.
Consider re-validating after the useCwd override:
if (validation.valid && "useCwd" in validation && validation.useCwd) {
targetDir = process.cwd();
projectName = path.basename(process.cwd());
// Re-validate the derived name
const revalidation = validateProjectName(projectName);
if (!revalidation.valid) {
console.error(`Invalid project name derived from cwd: ${revalidation.message}`);
process.exit(1);
}
if (revalidation.valid && revalidation.normalized) {
projectName = revalidation.normalized;
}
}| const answers = await p.group({ | ||
| projectName: () => | ||
| defaults.projectName | ||
| ? Promise.resolve(defaults.projectName) | ||
| : p.text({ | ||
| message: "Project name:", | ||
| placeholder: "my-vinext-app", | ||
| validate: (value) => { | ||
| if (!value.trim()) return "Project name is required"; | ||
| // Basic validation inline — full validation happens after prompts | ||
| }, | ||
| }), | ||
| template: () => | ||
| defaults.template | ||
| ? Promise.resolve(defaults.template) | ||
| : p.select({ | ||
| message: "Which router?", | ||
| options: [ | ||
| { value: "app" as const, label: "App Router", hint: "recommended" }, | ||
| { value: "pages" as const, label: "Pages Router" }, | ||
| ], | ||
| }), | ||
| }); |
There was a problem hiding this comment.
Potential issue: p.group() default cancel behavior.
@clack/prompts's group() function, when called without an onCancel callback, throws a CancelError on Ctrl+C rather than returning a value. So p.isCancel(answers) at line 41 would never be reached — the thrown error would propagate to main() and be caught by the .catch() handler, printing a stack trace.
To make cancellation graceful, you need to either:
- Pass
onCanceltop.group()that returns (not throws) - Wrap the
p.group()call in a try-catch
The previous version had onCancel: () => process.exit(0) which was a different bug. The current version may silently work if @clack/prompts v0.10 changed the default behavior, but it's worth verifying. A defensive approach:
try {
const answers = await p.group({ ... });
if (p.isCancel(answers)) {
p.cancel("Cancelled.");
return null;
}
return answers as PromptAnswers;
} catch {
p.cancel("Cancelled.");
return null;
}| copyDir(templateDir, projectPath); | ||
|
|
||
| // Substitute .tmpl variables | ||
| // Template uses "vinext": "latest" in package.json. Pin to caret range once vinext reaches 1.0. |
There was a problem hiding this comment.
The comment acknowledges the issue but "latest" is risky even pre-1.0. Since vinext is in active development with potentially breaking changes between minor versions, latest means scaffolded projects are not reproducible.
A pragmatic approach: read the current version of vinext from the monorepo at scaffold time and inject it:
// In scaffold.ts or index.ts
const vinextPkg = JSON.parse(
fs.readFileSync(path.resolve(__dirname, '../../vinext/package.json'), 'utf-8')
);
// Then add to tmpl vars:
"{{VINEXT_VERSION}}": `^${vinextPkg.version}`Or simpler: since the publish workflow locks both packages to the same version, use the CLI's own version:
"{{VINEXT_VERSION}}": `^${VERSION}`Then in the template: "vinext": "{{VINEXT_VERSION}}"
| ...(env.optimizeDeps.include ?? []), | ||
| ...reactDeps.filter((d) => !(env.optimizeDeps!.include ?? []).includes(d)), | ||
| ]; | ||
| }, |
There was a problem hiding this comment.
Unrelated change — should be a separate PR.
This configEnvironment hook that adds React dependencies to optimizeDeps.include for server environments when Cloudflare plugin is present is a meaningful fix, but it's:
- Completely unrelated to the scaffolding CLI
- Missing test coverage (no test verifies the behavior)
- Potentially affecting all vinext users, not just scaffolded projects
Please split this into its own PR with a test or at minimum a repro description of what breaks without it.
| "build": "vite build", | ||
| "preview": "vite preview" | ||
| }, | ||
| "dependencies": { |
There was a problem hiding this comment.
Minor: all deps are in dependencies, none in devDependencies.
typescript, wrangler, and @cloudflare/workers-types (referenced in tsconfig.json but not listed here) are typically devDependencies. Also, @cloudflare/workers-types is referenced in the tsconfig "types" field but missing from the package.json entirely.
Consider:
"dependencies": {
"react": "^19.2.4",
"react-dom": "^19.2.4",
"vinext": "latest",
"@vitejs/plugin-rsc": "^0.5.19",
"react-server-dom-webpack": "^19.2.4"
},
"devDependencies": {
"vite": "^7.3.1",
"wrangler": "^4.65.0",
"@cloudflare/vite-plugin": "^1.25.0",
"@cloudflare/workers-types": "^4.x",
"typescript": "^5.8.0"
}|
|
||
| - name: Publish create-vinext-app (OIDC trusted publishing) | ||
| working-directory: packages/create-vinext-app | ||
| run: npm publish --access public --provenance |
There was a problem hiding this comment.
npm publish vs pnpm publish inconsistency.
Line 82 uses pnpm publish for vinext, but line 86 uses npm publish for create-vinext-app. Since this is a pnpm monorepo and both packages are workspaces, using different publish commands is inconsistent. Is this intentional (e.g., because create-vinext-app has no workspace dependencies)?
If it's a pnpm workspace, pnpm publish handles workspace protocol resolution (workspace:* → actual version). That doesn't apply here since create-vinext-app doesn't depend on vinext, but using the same tool for both avoids confusion.
| run: npm publish --access public --provenance | |
| run: pnpm publish --access public --provenance |
|
Review posted. Here's a summary of what I found: Previously flagged issues — 7 of 10 were addressed:
New issues found (7 inline comments):
|
Summary
Closes #407
Adds
packages/create-vinext-app/— a standalone CLI fornpm create vinext-app@latestthat scaffolds new vinext projects targeting Cloudflare Workers with a single command.Previously, starting a new vinext project required
npm create next-app@latest→vinext init→ manually creatingwrangler.jsonc,worker/index.ts, and installing extra deps (8-12 manual steps). This reduces it to one command.@vitejs/plugin-rsc) and Pages Router (with worker entry)@clack/promptswith--yesflag for CI/non-interactive usagenpm_config_user_agent(works with npm/pnpm/yarn/bun).tmplvariable substitution for{{PROJECT_NAME}}and{{WORKER_NAME}}Before merging: npm setup required
Test plan
pnpm --filter create-vinext-app test— 102/102 tests passpnpm run build— both packages buildpnpm run lint && pnpm run fmt:check && pnpm run typecheck— all cleancreate-vinext-appjob passes on ubuntu + windows