void 0.1.6 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_PROMPT.md +15 -0
- package/README.md +62 -123
- package/dist/auth-BdsJ0Aff.d.mts +43 -0
- package/dist/auth-cmd-Dx8oPKZC.mjs +43 -0
- package/dist/auth-migrations-BAtAck2g.mjs +117 -0
- package/dist/better-auth-shared-C9_GHSkR.d.mts +71 -0
- package/dist/better-auth-shared-CdYmQGry.mjs +163 -0
- package/dist/cache-W82I8ihI.mjs +47 -0
- package/dist/cancel-deploy-BOBTqqh0.mjs +59 -0
- package/dist/cf-access-Dee5cXxL.mjs +22 -0
- package/dist/chunk-DJd-R1mw.mjs +34 -0
- package/dist/cli/cli.d.mts +1 -0
- package/dist/cli/cli.mjs +1807 -0
- package/dist/client-snXOjrp1.mjs +565 -0
- package/dist/collect-CjeZgz5D.mjs +55 -0
- package/dist/config-BIa9HwVX.mjs +573 -0
- package/dist/config-BzM9Dy7T.mjs +37 -0
- package/dist/config-CvHtTM0q.mjs +30 -0
- package/dist/create-project-BIA15W7z.mjs +90 -0
- package/dist/db-DsRoMcfN.mjs +895 -0
- package/dist/defer-DcxEsVH1.mjs +49 -0
- package/dist/delete-DAP6yDc7.mjs +64 -0
- package/dist/deploy-BPKblFx6.mjs +2424 -0
- package/dist/discover-B7FkXBLB.mjs +40 -0
- package/dist/dist-DUyXJLkq.mjs +2667 -0
- package/dist/dist-Dayj3gCK.mjs +1287 -0
- package/dist/domain-BGofcQ6I.mjs +79 -0
- package/dist/dotenv-DwO4ti0Z.mjs +173 -0
- package/dist/drizzle-NnudE_UN.mjs +232 -0
- package/dist/env-CyG3tvU0.mjs +301 -0
- package/dist/env-helpers-Dr9Y7RnE.d.mts +52 -0
- package/dist/env-raw-BDL4TvdN.mjs +32 -0
- package/dist/env-types-DknSA4SO.mjs +64 -0
- package/dist/env-validation-DJKjR_8q.mjs +163 -0
- package/dist/fetch-error-BQ8sZ5Nd.mjs +266 -0
- package/dist/fetch-error-CVZ5CGA-.d.mts +20 -0
- package/dist/gen-U0Ktr4Zd.mjs +761 -0
- package/dist/handler-B0ds0OHJ.d.mts +269 -0
- package/dist/head-P-egrtFE.d.mts +45 -0
- package/dist/headers-DCXc7mDs.mjs +279 -0
- package/dist/index.d.mts +32 -0
- package/dist/index.mjs +4695 -0
- package/dist/init-C7wS5iGP.mjs +2625 -0
- package/dist/link-p2R6NbgN.mjs +49 -0
- package/dist/list-Bfel-QLc.mjs +113 -0
- package/dist/log-DXdqnmhF.mjs +26 -0
- package/dist/login-CkcXUiIu.mjs +72 -0
- package/dist/logs-DmkrRvx6.mjs +98 -0
- package/dist/magic-string.es-D6g9UnIy.mjs +1011 -0
- package/dist/mcp-CaQzfeUi.mjs +373 -0
- package/dist/node-DDfXj10V.mjs +54 -0
- package/dist/output-BwlcIYSR.mjs +139 -0
- package/dist/pages/client.d.mts +198 -0
- package/dist/pages/client.mjs +980 -0
- package/dist/pages/head-client.d.mts +15 -0
- package/dist/pages/head-client.mjs +90 -0
- package/dist/pages/head.d.mts +2 -0
- package/dist/pages/head.mjs +112 -0
- package/dist/pages/index.d.mts +38 -0
- package/dist/pages/index.mjs +76 -0
- package/dist/pages/islands-plugin.d.mts +50 -0
- package/dist/pages/islands-plugin.mjs +195 -0
- package/dist/pages/prefetch.d.mts +31 -0
- package/dist/pages/prefetch.mjs +90 -0
- package/dist/pages/protocol.d.mts +3 -0
- package/dist/pages/protocol.mjs +193 -0
- package/dist/pages/serialize.d.mts +10 -0
- package/dist/pages/serialize.mjs +14 -0
- package/dist/pathe.M-eThtNZ-D-kmWkCS.mjs +150 -0
- package/dist/plugin-inference-oZ6Ybu2_.mjs +2447 -0
- package/dist/prepare-BAtWufvm.mjs +99 -0
- package/dist/preset-D4I73kT4.mjs +221 -0
- package/dist/project-TqORyHn8.mjs +72 -0
- package/dist/project-cmd-B7lQp3F3.mjs +67 -0
- package/dist/project-slug-CKam8lF9.mjs +11 -0
- package/dist/project-tsconfig-DfkESbDL.mjs +63 -0
- package/dist/protocol-BWzXs2A2.d.mts +34 -0
- package/dist/providers-B3aMxWzP.mjs +67 -0
- package/dist/resolve-project-Br5BR03U.mjs +29 -0
- package/dist/rollback-gyC59l7U.mjs +92 -0
- package/dist/route-types-DReF1gUY.mjs +255 -0
- package/dist/routes-stub.d.mts +55 -0
- package/dist/routes-stub.mjs +1 -0
- package/dist/runner-6Ep3fNQu.mjs +123 -0
- package/dist/runner-pg-D0wWHYnr.mjs +57 -0
- package/dist/runtime/ai.d.mts +127 -0
- package/dist/runtime/ai.mjs +348 -0
- package/dist/runtime/auth-client-react.d.mts +8 -0
- package/dist/runtime/auth-client-react.mjs +6 -0
- package/dist/runtime/auth-client-solid.d.mts +8 -0
- package/dist/runtime/auth-client-solid.mjs +6 -0
- package/dist/runtime/auth-client-svelte.d.mts +8 -0
- package/dist/runtime/auth-client-svelte.mjs +6 -0
- package/dist/runtime/auth-client-vue.d.mts +8 -0
- package/dist/runtime/auth-client-vue.mjs +6 -0
- package/dist/runtime/auth-client.d.mts +8 -0
- package/dist/runtime/auth-client.mjs +6 -0
- package/dist/runtime/auth.d.mts +2 -0
- package/dist/runtime/auth.mjs +22 -0
- package/dist/runtime/better-auth-pg.d.mts +11 -0
- package/dist/runtime/better-auth-pg.mjs +51 -0
- package/dist/runtime/better-auth.d.mts +11 -0
- package/dist/runtime/better-auth.mjs +33 -0
- package/dist/runtime/client.d.mts +6 -0
- package/dist/runtime/client.mjs +5 -0
- package/dist/runtime/db-pg.d.mts +2 -0
- package/dist/runtime/db-pg.mjs +1 -0
- package/dist/runtime/db.d.mts +17 -0
- package/dist/runtime/db.mjs +30 -0
- package/dist/runtime/drizzle-arktype.d.mts +1 -0
- package/dist/runtime/drizzle-arktype.mjs +2 -0
- package/dist/runtime/drizzle-valibot.d.mts +1 -0
- package/dist/runtime/drizzle-valibot.mjs +2 -0
- package/dist/runtime/drizzle-zod.d.mts +1 -0
- package/dist/runtime/drizzle-zod.mjs +2 -0
- package/dist/runtime/env-helpers.d.mts +2 -0
- package/dist/runtime/env-helpers.mjs +173 -0
- package/dist/runtime/env-public-client.d.mts +22 -0
- package/dist/runtime/env-public-client.mjs +54 -0
- package/dist/runtime/env-public.d.mts +143 -0
- package/dist/runtime/env-public.mjs +366 -0
- package/dist/runtime/env.d.mts +13 -0
- package/dist/runtime/env.mjs +51 -0
- package/dist/runtime/fetch-stream.d.mts +51 -0
- package/dist/runtime/fetch-stream.mjs +81 -0
- package/dist/runtime/fetch.d.mts +59 -0
- package/dist/runtime/fetch.mjs +18 -0
- package/dist/runtime/handler.d.mts +3 -0
- package/dist/runtime/handler.mjs +85 -0
- package/dist/runtime/isr.d.mts +26 -0
- package/dist/runtime/isr.mjs +43 -0
- package/dist/runtime/kv.d.mts +48 -0
- package/dist/runtime/kv.mjs +106 -0
- package/dist/runtime/log.d.mts +24 -0
- package/dist/runtime/log.mjs +31 -0
- package/dist/runtime/migration-handler-pg.d.mts +6 -0
- package/dist/runtime/migration-handler-pg.mjs +85 -0
- package/dist/runtime/migration-handler.d.mts +19 -0
- package/dist/runtime/migration-handler.mjs +92 -0
- package/dist/runtime/queues.d.mts +7 -0
- package/dist/runtime/queues.mjs +8 -0
- package/dist/runtime/remote/binding-handler.d.mts +15 -0
- package/dist/runtime/remote/binding-handler.mjs +208 -0
- package/dist/runtime/remote/index.d.mts +8 -0
- package/dist/runtime/remote/index.mjs +461 -0
- package/dist/runtime/response.d.mts +14 -0
- package/dist/runtime/response.mjs +30 -0
- package/dist/runtime/sandbox.d.mts +17 -0
- package/dist/runtime/sandbox.mjs +19 -0
- package/dist/runtime/schema-d1.d.mts +1 -0
- package/dist/runtime/schema-d1.mjs +2 -0
- package/dist/runtime/schema-pg.d.mts +1 -0
- package/dist/runtime/schema-pg.mjs +2 -0
- package/dist/runtime/seed.d.mts +30 -0
- package/dist/runtime/seed.mjs +6 -0
- package/dist/runtime/storage.d.mts +7 -0
- package/dist/runtime/storage.mjs +14 -0
- package/dist/runtime/validator.d.mts +2 -0
- package/dist/runtime/validator.mjs +72 -0
- package/dist/runtime/ws-server.d.mts +26 -0
- package/dist/runtime/ws-server.mjs +296 -0
- package/dist/runtime/ws.d.mts +123 -0
- package/dist/runtime/ws.mjs +103 -0
- package/dist/scan-Ba4hFwlH.mjs +324 -0
- package/dist/scan-C6HMEIdW.mjs +318 -0
- package/dist/secret-CeRSukgM.mjs +109 -0
- package/dist/skills-ipldjlKE.mjs +62 -0
- package/dist/standard-schema-9CRjx-uR.d.mts +42 -0
- package/dist/subcommand-prompt-BKjuNAPb.mjs +349 -0
- package/dist/sveltekit.d.mts +20 -0
- package/dist/sveltekit.mjs +61 -0
- package/dist/types-mHOEwpW4.d.mts +57 -0
- package/dist/validate-CaMavMxu.mjs +146 -0
- package/dist/yarn-pnp-BFqMV_bl.mjs +196 -0
- package/getting-started-prompt.txt +26 -0
- package/package.json +322 -30
- package/schema.json +364 -0
- package/skills/migrate-vite-cloudflare-to-void/SKILL.md +175 -0
- package/skills/void/SKILL.md +75 -0
- package/skills/void/command/void.md +7 -0
- package/skills/void/docs/guide/ai.md +235 -0
- package/skills/void/docs/guide/app-types.md +103 -0
- package/skills/void/docs/guide/auth.md +257 -0
- package/skills/void/docs/guide/database/d1.md +106 -0
- package/skills/void/docs/guide/database/postgresql.md +106 -0
- package/skills/void/docs/guide/database.md +418 -0
- package/skills/void/docs/guide/deployment.md +98 -0
- package/skills/void/docs/guide/edge/headers.md +79 -0
- package/skills/void/docs/guide/edge/prerendering.md +83 -0
- package/skills/void/docs/guide/edge/redirects.md +116 -0
- package/skills/void/docs/guide/edge/revalidation.md +131 -0
- package/skills/void/docs/guide/edge/rewrites.md +354 -0
- package/skills/void/docs/guide/edge/static-assets.md +72 -0
- package/skills/void/docs/guide/env-vars.md +298 -0
- package/skills/void/docs/guide/index.md +80 -0
- package/skills/void/docs/guide/jobs.md +91 -0
- package/skills/void/docs/guide/kv.md +107 -0
- package/skills/void/docs/guide/pages-routing/actions-and-forms.md +419 -0
- package/skills/void/docs/guide/pages-routing/head.md +130 -0
- package/skills/void/docs/guide/pages-routing/islands.md +405 -0
- package/skills/void/docs/guide/pages-routing/layouts.md +362 -0
- package/skills/void/docs/guide/pages-routing/loaders.md +267 -0
- package/skills/void/docs/guide/pages-routing/markdown.md +625 -0
- package/skills/void/docs/guide/pages-routing/overview.md +236 -0
- package/skills/void/docs/guide/pages-routing/view-transitions.md +140 -0
- package/skills/void/docs/guide/queues.md +140 -0
- package/skills/void/docs/guide/quickstart.md +233 -0
- package/skills/void/docs/guide/remote-dev.md +67 -0
- package/skills/void/docs/guide/sandboxes.md +82 -0
- package/skills/void/docs/guide/server-routing.md +246 -0
- package/skills/void/docs/guide/ssg.md +74 -0
- package/skills/void/docs/guide/ssr.md +105 -0
- package/skills/void/docs/guide/storage.md +67 -0
- package/skills/void/docs/guide/type-safety.md +179 -0
- package/skills/void/docs/guide/typed-fetch.md +113 -0
- package/skills/void/docs/guide/websockets.md +190 -0
- package/skills/void/docs/index.md +48 -0
- package/skills/void/docs/integrations/agents.md +84 -0
- package/skills/void/docs/integrations/cloudflare.md +284 -0
- package/skills/void/docs/integrations/frameworks/analog.md +182 -0
- package/skills/void/docs/integrations/frameworks/astro.md +197 -0
- package/skills/void/docs/integrations/frameworks/nuxt.md +164 -0
- package/skills/void/docs/integrations/frameworks/overview.md +136 -0
- package/skills/void/docs/integrations/frameworks/react-router.md +137 -0
- package/skills/void/docs/integrations/frameworks/sveltekit.md +191 -0
- package/skills/void/docs/integrations/frameworks/tanstack-start.md +140 -0
- package/skills/void/docs/integrations/hono.md +97 -0
- package/skills/void/docs/integrations/nodejs-bun-deno.md +210 -0
- package/skills/void/docs/node_modules/@iconify/vue/README.md +408 -0
- package/skills/void/docs/node_modules/@iconify/vue/offline/readme.md +5 -0
- package/skills/void/docs/node_modules/@voidzero-dev/vitepress-theme/README.md +103 -0
- package/skills/void/docs/node_modules/oxc-minify/README.md +78 -0
- package/skills/void/docs/node_modules/reka-ui/README.md +80 -0
- package/skills/void/docs/node_modules/vitepress/README.md +28 -0
- package/skills/void/docs/node_modules/vitepress/template/api-examples.md +49 -0
- package/skills/void/docs/node_modules/vitepress/template/index.md +28 -0
- package/skills/void/docs/node_modules/vitepress/template/markdown-examples.md +85 -0
- package/skills/void/docs/node_modules/vitepress-plugin-group-icons/README.md +101 -0
- package/skills/void/docs/node_modules/void/AGENTS.md +204 -0
- package/skills/void/docs/node_modules/void/AGENT_PROMPT.md +15 -0
- package/skills/void/docs/node_modules/void/README.md +89 -0
- package/skills/void/docs/node_modules/void/node_modules/@clack/prompts/CHANGELOG.md +591 -0
- package/skills/void/docs/node_modules/void/node_modules/@clack/prompts/README.md +375 -0
- package/skills/void/docs/node_modules/void/node_modules/@cloudflare/sandbox/README.md +174 -0
- package/skills/void/docs/node_modules/void/node_modules/@cloudflare/vite-plugin/README.md +37 -0
- package/skills/void/docs/node_modules/void/node_modules/@cloudflare/workers-types/README.md +135 -0
- package/skills/void/docs/node_modules/void/node_modules/@electric-sql/pglite/README.md +189 -0
- package/skills/void/docs/node_modules/void/node_modules/@hono/oauth-providers/CHANGELOG.md +143 -0
- package/skills/void/docs/node_modules/void/node_modules/@hono/oauth-providers/README.md +1272 -0
- package/skills/void/docs/node_modules/void/node_modules/@napi-rs/keyring/README.md +19 -0
- package/skills/void/docs/node_modules/void/node_modules/@types/better-sqlite3/README.md +15 -0
- package/skills/void/docs/node_modules/void/node_modules/@types/node/README.md +15 -0
- package/skills/void/docs/node_modules/void/node_modules/@types/pg/README.md +15 -0
- package/skills/void/docs/node_modules/void/node_modules/@typescript/native-preview/README.md +22 -0
- package/skills/void/docs/node_modules/void/node_modules/@typescript/native-preview/vendor/vscode-jsonrpc/README.md +69 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/README.md +152 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/@shikijs/engine-javascript/README.md +9 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/@shikijs/transformers/README.md +9 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/@types/node/README.md +15 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/gray-matter/CHANGELOG.md +24 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/gray-matter/README.md +565 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-exit/README.md +124 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-it-anchor/README.md +600 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-it-attrs/README.md +386 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-it-container/README.md +95 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-it-emoji/README.md +101 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/pathe/README.md +73 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/shiki/README.md +15 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/tinyglobby/README.md +25 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/tsdown/README.md +55 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite/LICENSE.md +2230 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite/README.md +20 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vue/README.md +58 -0
- package/skills/void/docs/node_modules/void/node_modules/arktype/README.md +165 -0
- package/skills/void/docs/node_modules/void/node_modules/better-auth/LICENSE.md +20 -0
- package/skills/void/docs/node_modules/void/node_modules/better-auth/README.md +32 -0
- package/skills/void/docs/node_modules/void/node_modules/better-sqlite3/README.md +99 -0
- package/skills/void/docs/node_modules/void/node_modules/blake3-jit/README.md +108 -0
- package/skills/void/docs/node_modules/void/node_modules/drizzle-arktype/README.md +51 -0
- package/skills/void/docs/node_modules/void/node_modules/drizzle-kit/README.md +79 -0
- package/skills/void/docs/node_modules/void/node_modules/drizzle-orm/README.md +44 -0
- package/skills/void/docs/node_modules/void/node_modules/drizzle-valibot/README.md +51 -0
- package/skills/void/docs/node_modules/void/node_modules/drizzle-zod/README.md +65 -0
- package/skills/void/docs/node_modules/void/node_modules/es-module-lexer/README.md +390 -0
- package/skills/void/docs/node_modules/void/node_modules/estree-walker/README.md +48 -0
- package/skills/void/docs/node_modules/void/node_modules/hono/README.md +85 -0
- package/skills/void/docs/node_modules/void/node_modules/ignore/README.md +452 -0
- package/skills/void/docs/node_modules/void/node_modules/jsonc-parser/CHANGELOG.md +76 -0
- package/{LICENSE → skills/void/docs/node_modules/void/node_modules/jsonc-parser/LICENSE.md} +21 -21
- package/skills/void/docs/node_modules/void/node_modules/jsonc-parser/README.md +364 -0
- package/skills/void/docs/node_modules/void/node_modules/jsonc-parser/SECURITY.md +41 -0
- package/skills/void/docs/node_modules/void/node_modules/magic-string/README.md +325 -0
- package/skills/void/docs/node_modules/void/node_modules/ofetch/README.md +398 -0
- package/skills/void/docs/node_modules/void/node_modules/pathe/README.md +73 -0
- package/skills/void/docs/node_modules/void/node_modules/pg/README.md +95 -0
- package/skills/void/docs/node_modules/void/node_modules/pglite-server/LICENSE.md +21 -0
- package/skills/void/docs/node_modules/void/node_modules/pglite-server/README.md +135 -0
- package/skills/void/docs/node_modules/void/node_modules/picocolors/README.md +21 -0
- package/skills/void/docs/node_modules/void/node_modules/tinyglobby/README.md +25 -0
- package/skills/void/docs/node_modules/void/node_modules/tsdown/README.md +55 -0
- package/skills/void/docs/node_modules/void/node_modules/valibot/LICENSE.md +9 -0
- package/skills/void/docs/node_modules/void/node_modules/valibot/README.md +94 -0
- package/skills/void/docs/node_modules/void/node_modules/vite/LICENSE.md +2230 -0
- package/skills/void/docs/node_modules/void/node_modules/vite/README.md +20 -0
- package/skills/void/docs/node_modules/void/node_modules/wrangler/README.md +63 -0
- package/skills/void/docs/node_modules/void/node_modules/zod/README.md +191 -0
- package/skills/void/docs/node_modules/void/skills/migrate-vite-cloudflare-to-void/SKILL.md +175 -0
- package/skills/void/docs/node_modules/void/skills/void/SKILL.md +75 -0
- package/skills/void/docs/node_modules/void/skills/void/command/void.md +7 -0
- package/skills/void/docs/reference/api.md +917 -0
- package/skills/void/docs/reference/cli.md +561 -0
- package/skills/void/docs/reference/config.md +408 -0
- package/skills/void/docs/reference/resource-inference.md +149 -0
- package/skills/void/docs/reference/structure.md +176 -0
- package/.npmignore +0 -29
- package/.travis.yml +0 -9
- package/favicon.ico +0 -0
- package/index.js +0 -14
- package/lib/Job.js +0 -150
- package/lib/Void.js +0 -99
- package/lib/scan.js +0 -19
- package/test/credentials.js +0 -20
- package/test/job.js +0 -64
- package/test/static/dir1/test6.html +0 -0
- package/test/static/dir2/test7.html +0 -0
- package/test/static/dir2/test8.html +0 -0
- package/test/static/dir2/test9.html +0 -0
- package/test/static/test1.html +0 -0
- package/test/static/test2.html +0 -0
- package/test/static/test3.html +0 -0
- package/test/void.js +0 -31
- /package/{test/static/dir1/test4.html → skills/void/docs/integrations/auth-providers.md} +0 -0
- /package/{test/static/dir1/test5.html → skills/void/docs/integrations/payment-processors.md} +0 -0
|
@@ -0,0 +1,2625 @@
|
|
|
1
|
+
import { a as join, i as isAbsolute, n as dirname, o as relative, s as resolve, t as basename } from "./pathe.M-eThtNZ-D-kmWkCS.mjs";
|
|
2
|
+
import { n as cliTitle } from "./output-BwlcIYSR.mjs";
|
|
3
|
+
import { _ as me, c as R, g as ge, i as Ee, u as Se, v as ue, x as q, y as ye } from "./dist-Dayj3gCK.mjs";
|
|
4
|
+
import { a as writeProjectConfig, r as readProjectConfig } from "./project-TqORyHn8.mjs";
|
|
5
|
+
import { l as isStagingMode, n as PlatformClient, s as getToken } from "./client-snXOjrp1.mjs";
|
|
6
|
+
import { c as getDatabaseDialect, f as readConfig } from "./config-BIa9HwVX.mjs";
|
|
7
|
+
import { n as detectFramework } from "./plugin-inference-oZ6Ybu2_.mjs";
|
|
8
|
+
import { r as parseDotenvFile, t as expandDotenv } from "./dotenv-DwO4ti0Z.mjs";
|
|
9
|
+
import { n as ensureProjectTsconfig, t as createProjectTsconfig } from "./project-tsconfig-DfkESbDL.mjs";
|
|
10
|
+
import { t as MagicString } from "./magic-string.es-D6g9UnIy.mjs";
|
|
11
|
+
import { n as writeDrizzleConfig } from "./config-BzM9Dy7T.mjs";
|
|
12
|
+
import { n as promptProjectSelection, t as promptAndCreateProject } from "./create-project-BIA15W7z.mjs";
|
|
13
|
+
import { t as promptForLoginToken } from "./login-CkcXUiIu.mjs";
|
|
14
|
+
import { r as formatProjectCommand } from "./preset-D4I73kT4.mjs";
|
|
15
|
+
import { n as getAgentById, r as getPackageDir, t as detectAgents } from "./cli/cli.mjs";
|
|
16
|
+
import { n as findInstalledYarnPnpRuntime, r as hasLocalLockfile, t as buildDrizzleKitSpawnEnv } from "./yarn-pnp-BFqMV_bl.mjs";
|
|
17
|
+
import { createRequire } from "node:module";
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { parseArgs } from "node:util";
|
|
21
|
+
import { parse } from "jsonc-parser";
|
|
22
|
+
import { spawnSync } from "node:child_process";
|
|
23
|
+
//#region src/cli/deploy-setup.ts
|
|
24
|
+
async function ensureToken(root) {
|
|
25
|
+
const existing = getToken(root);
|
|
26
|
+
if (existing) return existing;
|
|
27
|
+
const token = await promptForLoginToken(root);
|
|
28
|
+
R.success(isStagingMode(root) ? "Staging token saved." : "Token saved.");
|
|
29
|
+
return token;
|
|
30
|
+
}
|
|
31
|
+
async function maybeSetupDeployProject(root) {
|
|
32
|
+
const existing = readProjectConfig(root);
|
|
33
|
+
if (existing) {
|
|
34
|
+
R.info(`Project already linked: ${existing.slug}`);
|
|
35
|
+
return existing;
|
|
36
|
+
}
|
|
37
|
+
const existingToken = getToken(root);
|
|
38
|
+
const shouldSetup = await ue({ message: existingToken ? "Link your Void project now?" : "Log in now and link or create a Void project?" });
|
|
39
|
+
if (q(shouldSetup)) {
|
|
40
|
+
me("Setup cancelled.");
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
if (!shouldSetup) return null;
|
|
44
|
+
const client = new PlatformClient(existingToken ?? await ensureToken(root), { root });
|
|
45
|
+
const projects = await client.listProjects();
|
|
46
|
+
if (projects.length === 0) return promptAndCreateProject(root, client);
|
|
47
|
+
const result = await promptProjectSelection(projects, "Select a project to link:", root);
|
|
48
|
+
if (result.action === "select") {
|
|
49
|
+
const config = {
|
|
50
|
+
projectId: result.project.id,
|
|
51
|
+
slug: result.project.slug
|
|
52
|
+
};
|
|
53
|
+
writeProjectConfig(root, config);
|
|
54
|
+
R.success(`Linked project: ${config.slug}`);
|
|
55
|
+
return config;
|
|
56
|
+
}
|
|
57
|
+
return promptAndCreateProject(root, client);
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/cli/env-scaffold.ts
|
|
61
|
+
/**
|
|
62
|
+
* `void init` hook — when the project has no `env.ts` but has dotenv files
|
|
63
|
+
* on disk, scaffold an `env.ts` whose keys mirror what the user already
|
|
64
|
+
* has. Inference is intentionally permissive: we only upgrade from the
|
|
65
|
+
* default `string()` helper when the value is unambiguously a boolean,
|
|
66
|
+
* URL, or small integer. Everything else stays a string and carries a
|
|
67
|
+
* one-line banner nudging the user to tighten the schema.
|
|
68
|
+
*
|
|
69
|
+
* This is a best-effort DX sweetener, not a correctness boundary — a
|
|
70
|
+
* wrong guess only costs the user a single-line edit.
|
|
71
|
+
*/
|
|
72
|
+
/** Dotenv files we harvest keys from, in trust order (first wins per key). */
|
|
73
|
+
const SCAFFOLD_SOURCE_FILES = [
|
|
74
|
+
".env",
|
|
75
|
+
".env.local",
|
|
76
|
+
".env.development",
|
|
77
|
+
".env.development.local",
|
|
78
|
+
".env.example"
|
|
79
|
+
];
|
|
80
|
+
const BOOLEAN_LITERAL = /^(true|false)$/i;
|
|
81
|
+
const HTTP_URL = /^https?:\/\/\S+$/;
|
|
82
|
+
const SMALL_INTEGER = /^(?:0|[1-9]\d{0,4})$/;
|
|
83
|
+
/**
|
|
84
|
+
* Classify a single `.env` value into the Void built-in helper to use.
|
|
85
|
+
* Returns `'string'` whenever inference is ambiguous — the comment in the
|
|
86
|
+
* scaffolded file asks the user to tighten any wrong guesses.
|
|
87
|
+
*/
|
|
88
|
+
function inferHelperForValue(value) {
|
|
89
|
+
if (!value) return "string";
|
|
90
|
+
const trimmed = value.trim();
|
|
91
|
+
if (!trimmed) return "string";
|
|
92
|
+
if (BOOLEAN_LITERAL.test(trimmed)) return "boolean";
|
|
93
|
+
if (HTTP_URL.test(trimmed)) return "url";
|
|
94
|
+
if (SMALL_INTEGER.test(trimmed)) return "number";
|
|
95
|
+
return "string";
|
|
96
|
+
}
|
|
97
|
+
/** Collect a deduped list of inferred entries from the dotenv files we find. */
|
|
98
|
+
function collectInferredEntries(root) {
|
|
99
|
+
const seen = /* @__PURE__ */ new Map();
|
|
100
|
+
const cumulative = {};
|
|
101
|
+
for (const file of SCAFFOLD_SOURCE_FILES) {
|
|
102
|
+
const parsed = parseDotenvFile(join(root, file), { expand: false });
|
|
103
|
+
const expanded = expandDotenv({
|
|
104
|
+
...parsed,
|
|
105
|
+
...cumulative
|
|
106
|
+
});
|
|
107
|
+
for (const key of Object.keys(parsed)) {
|
|
108
|
+
const value = expanded[key] ?? "";
|
|
109
|
+
if (!(key in cumulative)) cumulative[key] = parsed[key];
|
|
110
|
+
const existing = seen.get(key);
|
|
111
|
+
if (!existing) seen.set(key, {
|
|
112
|
+
key,
|
|
113
|
+
helper: inferHelperForValue(value),
|
|
114
|
+
sampleValue: value || void 0
|
|
115
|
+
});
|
|
116
|
+
else if (!existing.sampleValue && value) seen.set(key, {
|
|
117
|
+
key,
|
|
118
|
+
helper: inferHelperForValue(value),
|
|
119
|
+
sampleValue: value
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return [...seen.values()].sort((a, b) => a.key.localeCompare(b.key));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Render the scaffolded `env.ts` body. The imports are pulled in lazily
|
|
127
|
+
* so only helpers we reference appear in the import statement.
|
|
128
|
+
*/
|
|
129
|
+
function renderEnvTs(entries) {
|
|
130
|
+
if (entries.length === 0) return [
|
|
131
|
+
`import { defineEnv } from 'void/env';`,
|
|
132
|
+
``,
|
|
133
|
+
`// Define your environment variables here. See https://voidzero.dev/guide/env-vars`,
|
|
134
|
+
`export default defineEnv({});`,
|
|
135
|
+
``
|
|
136
|
+
].join("\n");
|
|
137
|
+
const helpers = new Set(["string"]);
|
|
138
|
+
for (const e of entries) helpers.add(e.helper);
|
|
139
|
+
const importList = [
|
|
140
|
+
"string",
|
|
141
|
+
"number",
|
|
142
|
+
"boolean",
|
|
143
|
+
"url"
|
|
144
|
+
].filter((h) => helpers.has(h));
|
|
145
|
+
const lines = [];
|
|
146
|
+
lines.push(`import { defineEnv, ${importList.join(", ")} } from 'void/env';`);
|
|
147
|
+
lines.push("");
|
|
148
|
+
lines.push(`// Scaffolded from your .env files. Inference is conservative — review each`);
|
|
149
|
+
lines.push(`// entry and tighten types as needed (e.g. oneOf([...]), url(), .optional(),`);
|
|
150
|
+
lines.push(`// .default(value), or a Standard Schema validator from valibot/zod/arktype).`);
|
|
151
|
+
lines.push(`export default defineEnv({`);
|
|
152
|
+
for (const entry of entries) lines.push(` ${entry.key}: ${entry.helper}(),`);
|
|
153
|
+
lines.push(`});`);
|
|
154
|
+
lines.push("");
|
|
155
|
+
return lines.join("\n");
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* If the project has no `env.ts`, scaffold one based on `.env*` files the
|
|
159
|
+
* user already has. Safe to call unconditionally during `void init` — it
|
|
160
|
+
* no-ops when `env.ts` is present. Never overwrites an existing file.
|
|
161
|
+
*/
|
|
162
|
+
function maybeScaffoldEnvTs(root) {
|
|
163
|
+
for (const candidate of [
|
|
164
|
+
"env.ts",
|
|
165
|
+
"env.mts",
|
|
166
|
+
"env.js",
|
|
167
|
+
"env.mjs"
|
|
168
|
+
]) if (existsSync(join(root, candidate))) return {
|
|
169
|
+
written: null,
|
|
170
|
+
skipReason: "env-ts-exists",
|
|
171
|
+
entries: []
|
|
172
|
+
};
|
|
173
|
+
for (const candidate of [
|
|
174
|
+
"src/env.ts",
|
|
175
|
+
"src/env.mts",
|
|
176
|
+
"src/env.js",
|
|
177
|
+
"src/env.mjs"
|
|
178
|
+
]) if (existsSync(join(root, candidate))) return {
|
|
179
|
+
written: null,
|
|
180
|
+
skipReason: "env-ts-exists",
|
|
181
|
+
entries: []
|
|
182
|
+
};
|
|
183
|
+
if (!SCAFFOLD_SOURCE_FILES.some((f) => existsSync(join(root, f)))) return {
|
|
184
|
+
written: null,
|
|
185
|
+
skipReason: "no-dotenv-files",
|
|
186
|
+
entries: []
|
|
187
|
+
};
|
|
188
|
+
const entries = collectInferredEntries(root);
|
|
189
|
+
const target = join(root, "env.ts");
|
|
190
|
+
writeFileSync(target, renderEnvTs(entries));
|
|
191
|
+
return {
|
|
192
|
+
written: target,
|
|
193
|
+
entries
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/cli/template.ts
|
|
198
|
+
const SCAFFOLD_GITIGNORE_LINES = [
|
|
199
|
+
"node_modules",
|
|
200
|
+
"dist",
|
|
201
|
+
".void",
|
|
202
|
+
".wrangler",
|
|
203
|
+
"*.tsbuildinfo",
|
|
204
|
+
".DS_Store"
|
|
205
|
+
];
|
|
206
|
+
const SCAFFOLD_BASE_DEPENDENCIES = { hono: "^4.11.9" };
|
|
207
|
+
const SCAFFOLD_BASE_DEV_DEPENDENCIES = {
|
|
208
|
+
vite: "^8.0.10",
|
|
209
|
+
typescript: "^5.9.3"
|
|
210
|
+
};
|
|
211
|
+
const REACT_SCAFFOLD_COMPILER_DEV_DEPENDENCIES = {
|
|
212
|
+
"@rolldown/plugin-babel": "^0.2.3",
|
|
213
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
214
|
+
"babel-plugin-react-compiler": "^1.0.0"
|
|
215
|
+
};
|
|
216
|
+
const SCAFFOLD_FRAMEWORK_OPTIONS = [
|
|
217
|
+
{
|
|
218
|
+
value: "react",
|
|
219
|
+
label: "React",
|
|
220
|
+
hint: "TSX Pages starter with the React adapter."
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
value: "vue",
|
|
224
|
+
label: "Vue",
|
|
225
|
+
hint: "Vue SFC Pages starter with the Vue adapter."
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
value: "svelte",
|
|
229
|
+
label: "Svelte",
|
|
230
|
+
hint: "Svelte 5 Pages starter with the Svelte adapter."
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
value: "solid",
|
|
234
|
+
label: "Solid",
|
|
235
|
+
hint: "Fine-grained TSX Pages starter with the Solid adapter."
|
|
236
|
+
}
|
|
237
|
+
];
|
|
238
|
+
const SCAFFOLD_FRAMEWORKS = {
|
|
239
|
+
react: {
|
|
240
|
+
label: "React",
|
|
241
|
+
adapterPackage: "@void/react",
|
|
242
|
+
adapterImport: "voidReact",
|
|
243
|
+
adapterPlugin: "@void/react/plugin",
|
|
244
|
+
pageExtension: "tsx",
|
|
245
|
+
dependencies: {
|
|
246
|
+
react: "^19.2.4",
|
|
247
|
+
"react-dom": "^19.2.4"
|
|
248
|
+
},
|
|
249
|
+
devDependencies: {
|
|
250
|
+
...REACT_SCAFFOLD_COMPILER_DEV_DEPENDENCIES,
|
|
251
|
+
"@types/react": "^19.2.14",
|
|
252
|
+
"@types/react-dom": "^19.2.3"
|
|
253
|
+
},
|
|
254
|
+
tsconfigCompilerOptions: { jsx: "react-jsx" }
|
|
255
|
+
},
|
|
256
|
+
vue: {
|
|
257
|
+
label: "Vue",
|
|
258
|
+
adapterPackage: "@void/vue",
|
|
259
|
+
adapterImport: "voidVue",
|
|
260
|
+
adapterPlugin: "@void/vue/plugin",
|
|
261
|
+
pageExtension: "vue",
|
|
262
|
+
dependencies: { vue: "^3.5.30" },
|
|
263
|
+
devDependencies: {},
|
|
264
|
+
tsconfigCompilerOptions: { jsx: "preserve" }
|
|
265
|
+
},
|
|
266
|
+
svelte: {
|
|
267
|
+
label: "Svelte",
|
|
268
|
+
adapterPackage: "@void/svelte",
|
|
269
|
+
adapterImport: "voidSvelte",
|
|
270
|
+
adapterPlugin: "@void/svelte/plugin",
|
|
271
|
+
pageExtension: "svelte",
|
|
272
|
+
dependencies: { svelte: "^5.53.9" },
|
|
273
|
+
devDependencies: {},
|
|
274
|
+
tsconfigCompilerOptions: {}
|
|
275
|
+
},
|
|
276
|
+
solid: {
|
|
277
|
+
label: "Solid",
|
|
278
|
+
adapterPackage: "@void/solid",
|
|
279
|
+
adapterImport: "voidSolid",
|
|
280
|
+
adapterPlugin: "@void/solid/plugin",
|
|
281
|
+
pageExtension: "tsx",
|
|
282
|
+
dependencies: { "solid-js": "^1.9.11" },
|
|
283
|
+
devDependencies: {},
|
|
284
|
+
tsconfigCompilerOptions: {
|
|
285
|
+
jsx: "preserve",
|
|
286
|
+
jsxImportSource: "solid-js"
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
function detectScaffoldFrameworkFromDependencies(deps) {
|
|
291
|
+
for (const [framework, config] of Object.entries(SCAFFOLD_FRAMEWORKS)) if (config.adapterPackage in deps) return framework;
|
|
292
|
+
}
|
|
293
|
+
function getScaffoldTsconfigCompilerOptions(framework) {
|
|
294
|
+
return framework ? { ...SCAFFOLD_FRAMEWORKS[framework].tsconfigCompilerOptions } : {};
|
|
295
|
+
}
|
|
296
|
+
function createPagesStarterTemplate(options) {
|
|
297
|
+
const { framework, starterMode, voidPackageSpec, adapterPackageSpec } = options;
|
|
298
|
+
const config = SCAFFOLD_FRAMEWORKS[framework];
|
|
299
|
+
const files = {
|
|
300
|
+
"vite.config.ts": getViteConfig(framework, config),
|
|
301
|
+
[`pages/index.${config.pageExtension}`]: getIndexPage(framework, starterMode)
|
|
302
|
+
};
|
|
303
|
+
if (starterMode !== "static") {
|
|
304
|
+
files["pages/index.server.ts"] = getIndexServer();
|
|
305
|
+
files["routes/api/hello.ts"] = getHelloRoute(config.label, starterMode);
|
|
306
|
+
files["db/schema.ts"] = getSchemaFile(starterMode);
|
|
307
|
+
files["db/seed.ts"] = getSeedFile();
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
scripts: {
|
|
311
|
+
dev: "vite",
|
|
312
|
+
build: "vite build",
|
|
313
|
+
preview: "vite preview"
|
|
314
|
+
},
|
|
315
|
+
dependencies: {
|
|
316
|
+
...SCAFFOLD_BASE_DEPENDENCIES,
|
|
317
|
+
...config.dependencies
|
|
318
|
+
},
|
|
319
|
+
devDependencies: {
|
|
320
|
+
void: voidPackageSpec,
|
|
321
|
+
...SCAFFOLD_BASE_DEV_DEPENDENCIES,
|
|
322
|
+
[config.adapterPackage]: adapterPackageSpec,
|
|
323
|
+
...config.devDependencies
|
|
324
|
+
},
|
|
325
|
+
files
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function getViteConfig(framework, config) {
|
|
329
|
+
if (framework === "react") return `import babel from "@rolldown/plugin-babel";
|
|
330
|
+
import { reactCompilerPreset } from "@vitejs/plugin-react";
|
|
331
|
+
import { defineConfig } from "vite";
|
|
332
|
+
import { voidPlugin } from "void";
|
|
333
|
+
import { ${config.adapterImport} } from "${config.adapterPlugin}";
|
|
334
|
+
|
|
335
|
+
export default defineConfig({
|
|
336
|
+
plugins: [
|
|
337
|
+
babel({
|
|
338
|
+
presets: [reactCompilerPreset()],
|
|
339
|
+
}),
|
|
340
|
+
voidPlugin(),
|
|
341
|
+
${config.adapterImport}(),
|
|
342
|
+
],
|
|
343
|
+
});
|
|
344
|
+
`;
|
|
345
|
+
return `import { defineConfig } from "vite";
|
|
346
|
+
import { voidPlugin } from "void";
|
|
347
|
+
import { ${config.adapterImport} } from "${config.adapterPlugin}";
|
|
348
|
+
|
|
349
|
+
export default defineConfig({
|
|
350
|
+
plugins: [voidPlugin(), ${config.adapterImport}()],
|
|
351
|
+
});
|
|
352
|
+
`;
|
|
353
|
+
}
|
|
354
|
+
function getIndexServer() {
|
|
355
|
+
return `import { defineHandler, type InferProps } from "void";
|
|
356
|
+
import { db, desc } from "void/db";
|
|
357
|
+
import { messages } from "@schema";
|
|
358
|
+
|
|
359
|
+
export type Props = InferProps<typeof loader>;
|
|
360
|
+
|
|
361
|
+
export const loader = defineHandler(async () => {
|
|
362
|
+
const rows = await db.select().from(messages).orderBy(desc(messages.id));
|
|
363
|
+
return { messages: rows };
|
|
364
|
+
});
|
|
365
|
+
`;
|
|
366
|
+
}
|
|
367
|
+
function getHelloRoute(frameworkLabel, starterMode) {
|
|
368
|
+
return `import { defineHandler } from "void";
|
|
369
|
+
|
|
370
|
+
export const GET = defineHandler(() => {
|
|
371
|
+
return {
|
|
372
|
+
message: "Hello from Void",
|
|
373
|
+
framework: "${frameworkLabel}",
|
|
374
|
+
database: "${starterMode === "postgresql" ? "PostgreSQL" : "D1"}",
|
|
375
|
+
};
|
|
376
|
+
});
|
|
377
|
+
`;
|
|
378
|
+
}
|
|
379
|
+
function getSchemaFile(starterMode) {
|
|
380
|
+
if (starterMode === "postgresql") return `import { pgTable, serial, text, timestamp } from "void/schema-pg";
|
|
381
|
+
|
|
382
|
+
export const messages = pgTable("messages", {
|
|
383
|
+
id: serial("id").primaryKey(),
|
|
384
|
+
text: text("text").notNull(),
|
|
385
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
386
|
+
});
|
|
387
|
+
`;
|
|
388
|
+
return `import { sql } from "void/db";
|
|
389
|
+
import { integer, sqliteTable, text } from "void/schema-d1";
|
|
390
|
+
|
|
391
|
+
export const messages = sqliteTable("messages", {
|
|
392
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
393
|
+
text: text("text").notNull(),
|
|
394
|
+
createdAt: text("created_at").notNull().default(sql\`(datetime('now'))\`),
|
|
395
|
+
});
|
|
396
|
+
`;
|
|
397
|
+
}
|
|
398
|
+
function getSeedFile() {
|
|
399
|
+
return `import { defineSeed } from "void/seed";
|
|
400
|
+
|
|
401
|
+
const SAMPLE_MESSAGES = ["Hello from Void"];
|
|
402
|
+
|
|
403
|
+
export default defineSeed<typeof import("./schema")>(async ({ db, schema }) => {
|
|
404
|
+
await db.insert(schema.messages).values(
|
|
405
|
+
SAMPLE_MESSAGES.map((text) => ({
|
|
406
|
+
text,
|
|
407
|
+
})),
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
`;
|
|
411
|
+
}
|
|
412
|
+
function getIndexPage(framework, starterMode) {
|
|
413
|
+
if (starterMode === "static") switch (framework) {
|
|
414
|
+
case "react": return `export default function HomePage() {
|
|
415
|
+
return (
|
|
416
|
+
<main>
|
|
417
|
+
<h1>Welcome to Void</h1>
|
|
418
|
+
<p>This starter is running in Pages mode with no database or server code yet.</p>
|
|
419
|
+
<p>
|
|
420
|
+
Add <code>pages/*.server.ts</code>, <code>routes/</code>, or <code>void/db</code> when
|
|
421
|
+
you want to grow into full-stack features.
|
|
422
|
+
</p>
|
|
423
|
+
</main>
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
`;
|
|
427
|
+
case "vue": return `<template>
|
|
428
|
+
<main>
|
|
429
|
+
<h1>Welcome to Void</h1>
|
|
430
|
+
<p>This starter is running in Pages mode with no database or server code yet.</p>
|
|
431
|
+
<p>
|
|
432
|
+
Add <code>pages/*.server.ts</code>, <code>routes/</code>, or <code>void/db</code> when you
|
|
433
|
+
want to grow into full-stack features.
|
|
434
|
+
</p>
|
|
435
|
+
</main>
|
|
436
|
+
</template>
|
|
437
|
+
`;
|
|
438
|
+
case "svelte": return `<main>
|
|
439
|
+
<h1>Welcome to Void</h1>
|
|
440
|
+
<p>This starter is running in Pages mode with no database or server code yet.</p>
|
|
441
|
+
<p>
|
|
442
|
+
Add <code>pages/*.server.ts</code>, <code>routes/</code>, or <code>void/db</code> when you
|
|
443
|
+
want to grow into full-stack features.
|
|
444
|
+
</p>
|
|
445
|
+
</main>
|
|
446
|
+
`;
|
|
447
|
+
case "solid": return `export default function HomePage() {
|
|
448
|
+
return (
|
|
449
|
+
<main>
|
|
450
|
+
<h1>Welcome to Void</h1>
|
|
451
|
+
<p>This starter is running in Pages mode with no database or server code yet.</p>
|
|
452
|
+
<p>
|
|
453
|
+
Add <code>pages/*.server.ts</code>, <code>routes/</code>, or <code>void/db</code> when
|
|
454
|
+
you want to grow into full-stack features.
|
|
455
|
+
</p>
|
|
456
|
+
</main>
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
`;
|
|
460
|
+
}
|
|
461
|
+
switch (framework) {
|
|
462
|
+
case "react": return `import type { Props } from "./index.server";
|
|
463
|
+
|
|
464
|
+
export default function HomePage({ messages }: Props) {
|
|
465
|
+
return (
|
|
466
|
+
<main>
|
|
467
|
+
<h1>Welcome to Void</h1>
|
|
468
|
+
<p>
|
|
469
|
+
This starter is running in Pages mode. The list below is loaded from your database in
|
|
470
|
+
<code> pages/index.server.ts</code>.
|
|
471
|
+
</p>
|
|
472
|
+
<p>
|
|
473
|
+
API example: <a href="/api/hello">/api/hello</a>
|
|
474
|
+
</p>
|
|
475
|
+
{messages.length === 0 ? (
|
|
476
|
+
<p>
|
|
477
|
+
No messages yet. Run <code>void db seed</code> to load the sample row from
|
|
478
|
+
<code> db/seed.ts</code>.
|
|
479
|
+
</p>
|
|
480
|
+
) : (
|
|
481
|
+
<ul>
|
|
482
|
+
{messages.map((message) => (
|
|
483
|
+
<li key={message.id}>
|
|
484
|
+
<strong>{message.text}</strong>
|
|
485
|
+
<br />
|
|
486
|
+
<small>{String(message.createdAt)}</small>
|
|
487
|
+
</li>
|
|
488
|
+
))}
|
|
489
|
+
</ul>
|
|
490
|
+
)}
|
|
491
|
+
</main>
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
`;
|
|
495
|
+
case "vue": return `<script setup lang="ts">
|
|
496
|
+
import type { Props } from "./index.server";
|
|
497
|
+
|
|
498
|
+
defineProps<Props>();
|
|
499
|
+
<\/script>
|
|
500
|
+
|
|
501
|
+
<template>
|
|
502
|
+
<main>
|
|
503
|
+
<h1>Welcome to Void</h1>
|
|
504
|
+
<p>
|
|
505
|
+
This starter is running in Pages mode. The list below is loaded from your database in
|
|
506
|
+
<code>pages/index.server.ts</code>.
|
|
507
|
+
</p>
|
|
508
|
+
<p>
|
|
509
|
+
API example: <a href="/api/hello">/api/hello</a>
|
|
510
|
+
</p>
|
|
511
|
+
<p v-if="messages.length === 0">
|
|
512
|
+
No messages yet. Run <code>void db seed</code> to load the sample row from
|
|
513
|
+
<code>db/seed.ts</code>.
|
|
514
|
+
</p>
|
|
515
|
+
<ul v-else>
|
|
516
|
+
<li v-for="message in messages" :key="message.id">
|
|
517
|
+
<strong>{{ message.text }}</strong>
|
|
518
|
+
<br />
|
|
519
|
+
<small>{{ String(message.createdAt) }}</small>
|
|
520
|
+
</li>
|
|
521
|
+
</ul>
|
|
522
|
+
</main>
|
|
523
|
+
</template>
|
|
524
|
+
`;
|
|
525
|
+
case "svelte": return `<script lang="ts">
|
|
526
|
+
import type { Props } from "./index.server";
|
|
527
|
+
|
|
528
|
+
let { messages }: Props = $props();
|
|
529
|
+
<\/script>
|
|
530
|
+
|
|
531
|
+
<main>
|
|
532
|
+
<h1>Welcome to Void</h1>
|
|
533
|
+
<p>
|
|
534
|
+
This starter is running in Pages mode. The list below is loaded from your database in
|
|
535
|
+
<code>pages/index.server.ts</code>.
|
|
536
|
+
</p>
|
|
537
|
+
<p>
|
|
538
|
+
API example: <a href="/api/hello">/api/hello</a>
|
|
539
|
+
</p>
|
|
540
|
+
{#if messages.length === 0}
|
|
541
|
+
<p>
|
|
542
|
+
No messages yet. Run <code>void db seed</code> to load the sample row from
|
|
543
|
+
<code>db/seed.ts</code>.
|
|
544
|
+
</p>
|
|
545
|
+
{:else}
|
|
546
|
+
<ul>
|
|
547
|
+
{#each messages as message (message.id)}
|
|
548
|
+
<li>
|
|
549
|
+
<strong>{message.text}</strong>
|
|
550
|
+
<br />
|
|
551
|
+
<small>{String(message.createdAt)}</small>
|
|
552
|
+
</li>
|
|
553
|
+
{/each}
|
|
554
|
+
</ul>
|
|
555
|
+
{/if}
|
|
556
|
+
</main>
|
|
557
|
+
`;
|
|
558
|
+
case "solid": return `import type { Props } from "./index.server";
|
|
559
|
+
|
|
560
|
+
export default function HomePage(props: Props) {
|
|
561
|
+
return (
|
|
562
|
+
<main>
|
|
563
|
+
<h1>Welcome to Void</h1>
|
|
564
|
+
<p>
|
|
565
|
+
This starter is running in Pages mode. The list below is loaded from your database in
|
|
566
|
+
<code> pages/index.server.ts</code>.
|
|
567
|
+
</p>
|
|
568
|
+
<p>
|
|
569
|
+
API example: <a href="/api/hello">/api/hello</a>
|
|
570
|
+
</p>
|
|
571
|
+
{props.messages.length === 0 ? (
|
|
572
|
+
<p>
|
|
573
|
+
No messages yet. Run <code>void db seed</code> to load the sample row from
|
|
574
|
+
<code> db/seed.ts</code>.
|
|
575
|
+
</p>
|
|
576
|
+
) : (
|
|
577
|
+
<ul>
|
|
578
|
+
{props.messages.map((message) => (
|
|
579
|
+
<li>
|
|
580
|
+
<strong>{message.text}</strong>
|
|
581
|
+
<br />
|
|
582
|
+
<small>{String(message.createdAt)}</small>
|
|
583
|
+
</li>
|
|
584
|
+
))}
|
|
585
|
+
</ul>
|
|
586
|
+
)}
|
|
587
|
+
</main>
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
`;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
//#endregion
|
|
594
|
+
//#region src/cli/init.ts
|
|
595
|
+
function parseInitArgs(args) {
|
|
596
|
+
let parsed;
|
|
597
|
+
try {
|
|
598
|
+
parsed = parseArgs({
|
|
599
|
+
args,
|
|
600
|
+
options: {
|
|
601
|
+
tsconfig: { type: "boolean" },
|
|
602
|
+
github: { type: "boolean" },
|
|
603
|
+
agents: { type: "boolean" }
|
|
604
|
+
},
|
|
605
|
+
strict: true
|
|
606
|
+
});
|
|
607
|
+
} catch (error) {
|
|
608
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
609
|
+
throw new Error(`init: ${message}.`);
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
tsconfig: parsed.values.tsconfig === true,
|
|
613
|
+
github: parsed.values.github === true,
|
|
614
|
+
agents: parsed.values.agents === true
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
const EMPTY_PROJECT_ALLOWED_ENTRIES = new Set([
|
|
618
|
+
".git",
|
|
619
|
+
".gitignore",
|
|
620
|
+
"AGENTS.md",
|
|
621
|
+
"CLAUDE.md",
|
|
622
|
+
".node-version",
|
|
623
|
+
".npmrc",
|
|
624
|
+
".nvmrc",
|
|
625
|
+
".editorconfig",
|
|
626
|
+
".vscode",
|
|
627
|
+
".idea",
|
|
628
|
+
".DS_Store",
|
|
629
|
+
"node_modules",
|
|
630
|
+
"package.json",
|
|
631
|
+
"package-lock.json",
|
|
632
|
+
"pnpm-lock.yaml",
|
|
633
|
+
"yarn.lock",
|
|
634
|
+
"bun.lock",
|
|
635
|
+
"bun.lockb",
|
|
636
|
+
".agents",
|
|
637
|
+
".claude",
|
|
638
|
+
".cline",
|
|
639
|
+
".codex",
|
|
640
|
+
".continue",
|
|
641
|
+
".cursor",
|
|
642
|
+
".gemini",
|
|
643
|
+
".goose",
|
|
644
|
+
".junie",
|
|
645
|
+
".kilocode",
|
|
646
|
+
".kiro",
|
|
647
|
+
".opencode",
|
|
648
|
+
".qwen",
|
|
649
|
+
".roo",
|
|
650
|
+
".trae",
|
|
651
|
+
".windsurf",
|
|
652
|
+
".zencoder"
|
|
653
|
+
]);
|
|
654
|
+
const PNPM_ONLY_BUILT_DEPENDENCIES = [
|
|
655
|
+
"better-sqlite3",
|
|
656
|
+
"esbuild",
|
|
657
|
+
"workerd"
|
|
658
|
+
];
|
|
659
|
+
const drizzleKitBin = join(fileURLToPath(import.meta.resolve("drizzle-kit")), "..", "bin.cjs");
|
|
660
|
+
const EMPTY_PROJECT_ALLOWED_PACKAGES = new Set([
|
|
661
|
+
"void",
|
|
662
|
+
"@void/react",
|
|
663
|
+
"@void/vue",
|
|
664
|
+
"@void/svelte",
|
|
665
|
+
"@void/solid"
|
|
666
|
+
]);
|
|
667
|
+
const VITE_CONFIG_FILES = [
|
|
668
|
+
"vite.config.ts",
|
|
669
|
+
"vite.config.js",
|
|
670
|
+
"vite.config.mts",
|
|
671
|
+
"vite.config.mjs",
|
|
672
|
+
"vite.config.cts",
|
|
673
|
+
"vite.config.cjs"
|
|
674
|
+
];
|
|
675
|
+
const VOID_CONFIG_FILE = "void.json";
|
|
676
|
+
function asRecord(value) {
|
|
677
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
678
|
+
return value;
|
|
679
|
+
}
|
|
680
|
+
function parseJsoncFile(path) {
|
|
681
|
+
const errors = [];
|
|
682
|
+
const parsed = asRecord(parse(readFileSync(path, "utf-8"), errors));
|
|
683
|
+
return errors.length === 0 ? parsed : null;
|
|
684
|
+
}
|
|
685
|
+
function uniqueStrings(values) {
|
|
686
|
+
return Array.from(new Set(values));
|
|
687
|
+
}
|
|
688
|
+
function normalizeRelativePath(fromDir, toPath) {
|
|
689
|
+
const rel = relative(fromDir, toPath).replaceAll("\\", "/");
|
|
690
|
+
return rel === ".." || rel.startsWith("../") || rel.startsWith("./") ? rel : `./${rel}`;
|
|
691
|
+
}
|
|
692
|
+
function getOwnConfigBaseUrlDir(config, configDir) {
|
|
693
|
+
const compilerOptions = asRecord(config.compilerOptions);
|
|
694
|
+
return compilerOptions && typeof compilerOptions.baseUrl === "string" ? resolve(configDir, compilerOptions.baseUrl) : null;
|
|
695
|
+
}
|
|
696
|
+
function getEffectiveConfigBaseUrlDir(config, configPath, seen) {
|
|
697
|
+
if (seen.has(configPath)) return null;
|
|
698
|
+
seen.add(configPath);
|
|
699
|
+
const ownBaseUrl = getOwnConfigBaseUrlDir(config, dirname(configPath));
|
|
700
|
+
if (ownBaseUrl) return ownBaseUrl;
|
|
701
|
+
const extendsValues = typeof config.extends === "string" ? [config.extends] : Array.isArray(config.extends) ? config.extends.filter((value) => typeof value === "string") : [];
|
|
702
|
+
let inheritedBaseUrl = null;
|
|
703
|
+
for (const extendsValue of extendsValues) {
|
|
704
|
+
const extendedPath = resolveExtendedTsconfig(configPath, extendsValue);
|
|
705
|
+
if (!extendedPath || !existsSync(extendedPath)) continue;
|
|
706
|
+
const extendedConfig = parseJsoncFile(extendedPath);
|
|
707
|
+
if (!extendedConfig) continue;
|
|
708
|
+
inheritedBaseUrl = getEffectiveConfigBaseUrlDir(extendedConfig, extendedPath, seen) ?? inheritedBaseUrl;
|
|
709
|
+
}
|
|
710
|
+
return inheritedBaseUrl;
|
|
711
|
+
}
|
|
712
|
+
function effectiveConfigPathBaseDir(config, configPath) {
|
|
713
|
+
return getEffectiveConfigBaseUrlDir(config, configPath, /* @__PURE__ */ new Set()) ?? dirname(configPath);
|
|
714
|
+
}
|
|
715
|
+
function convertConfigFileEntry(value, configDir, root) {
|
|
716
|
+
if (isAbsolute(value)) return normalizeRelativePath(root, value);
|
|
717
|
+
return normalizeRelativePath(root, resolve(configDir, value));
|
|
718
|
+
}
|
|
719
|
+
function convertPathTarget(value, fromBaseDir, toBaseDir) {
|
|
720
|
+
if (isAbsolute(value)) return normalizeRelativePath(toBaseDir, value);
|
|
721
|
+
return normalizeRelativePath(toBaseDir, resolve(fromBaseDir, value));
|
|
722
|
+
}
|
|
723
|
+
function mergePathMaps(target, source) {
|
|
724
|
+
for (const [key, values] of Object.entries(source)) target[key] = values;
|
|
725
|
+
}
|
|
726
|
+
function readStringArray(value) {
|
|
727
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
728
|
+
}
|
|
729
|
+
function readPaths(value) {
|
|
730
|
+
const paths = asRecord(value);
|
|
731
|
+
if (!paths) return {};
|
|
732
|
+
const result = {};
|
|
733
|
+
for (const [key, values] of Object.entries(paths)) {
|
|
734
|
+
const stringValues = readStringArray(values);
|
|
735
|
+
if (stringValues.length > 0) result[key] = stringValues;
|
|
736
|
+
}
|
|
737
|
+
return result;
|
|
738
|
+
}
|
|
739
|
+
function resolveExtendedTsconfig(fromConfigPath, specifier) {
|
|
740
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
741
|
+
const resolved = resolve(dirname(fromConfigPath), specifier);
|
|
742
|
+
return resolved.endsWith(".json") ? resolved : `${resolved}.json`;
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
return createRequire(fromConfigPath).resolve(specifier);
|
|
746
|
+
} catch {
|
|
747
|
+
try {
|
|
748
|
+
return createRequire(fromConfigPath).resolve(`${specifier}.json`);
|
|
749
|
+
} catch {
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function collectTsconfigPatchFromConfig(config, configPath, root, outputBaseDir, seen) {
|
|
755
|
+
const patch = {
|
|
756
|
+
files: [],
|
|
757
|
+
paths: {}
|
|
758
|
+
};
|
|
759
|
+
if (seen.has(configPath)) return patch;
|
|
760
|
+
seen.add(configPath);
|
|
761
|
+
const extendsValues = typeof config.extends === "string" ? [config.extends] : Array.isArray(config.extends) ? config.extends.filter((value) => typeof value === "string") : [];
|
|
762
|
+
const hasOwnFiles = Array.isArray(config.files);
|
|
763
|
+
const compilerOptions = asRecord(config.compilerOptions);
|
|
764
|
+
const hasOwnPaths = asRecord(compilerOptions?.paths) !== null;
|
|
765
|
+
for (const extendsValue of extendsValues) {
|
|
766
|
+
const extendedPath = resolveExtendedTsconfig(configPath, extendsValue);
|
|
767
|
+
if (!extendedPath || !existsSync(extendedPath)) continue;
|
|
768
|
+
const extendedConfig = parseJsoncFile(extendedPath);
|
|
769
|
+
if (!extendedConfig) continue;
|
|
770
|
+
const extendedPatch = collectTsconfigPatchFromConfig(extendedConfig, extendedPath, root, outputBaseDir, seen);
|
|
771
|
+
if (!hasOwnFiles) patch.files.push(...extendedPatch.files);
|
|
772
|
+
if (!hasOwnPaths) mergePathMaps(patch.paths, extendedPatch.paths);
|
|
773
|
+
}
|
|
774
|
+
const configDir = dirname(configPath);
|
|
775
|
+
if (hasOwnFiles) patch.files.push(...readStringArray(config.files).map((entry) => convertConfigFileEntry(entry, configDir, root)));
|
|
776
|
+
if (hasOwnPaths) {
|
|
777
|
+
const paths = readPaths(compilerOptions?.paths);
|
|
778
|
+
const pathBaseDir = effectiveConfigPathBaseDir(config, configPath);
|
|
779
|
+
const convertedPaths = {};
|
|
780
|
+
for (const [key, values] of Object.entries(paths)) convertedPaths[key] = values.map((value) => convertPathTarget(value, pathBaseDir, outputBaseDir));
|
|
781
|
+
mergePathMaps(patch.paths, convertedPaths);
|
|
782
|
+
}
|
|
783
|
+
return patch;
|
|
784
|
+
}
|
|
785
|
+
function createVoidTsconfigPatch(root, outputBaseDir) {
|
|
786
|
+
const hasSchema = hasDrizzleSchema(root);
|
|
787
|
+
return collectTsconfigPatchFromConfig(createProjectTsconfig(root, void 0, hasSchema, hasSchema), join(root, ".void", "tsconfig.json"), root, outputBaseDir, /* @__PURE__ */ new Set());
|
|
788
|
+
}
|
|
789
|
+
function isScaffoldableEmptyProject(root) {
|
|
790
|
+
for (const dir of [
|
|
791
|
+
"src",
|
|
792
|
+
"routes",
|
|
793
|
+
"db",
|
|
794
|
+
"migrations",
|
|
795
|
+
"middleware",
|
|
796
|
+
"crons",
|
|
797
|
+
"queues",
|
|
798
|
+
"pages"
|
|
799
|
+
]) if (existsSync(join(root, dir))) return false;
|
|
800
|
+
if (existsSync(join(root, "index.html"))) return false;
|
|
801
|
+
for (const file of VITE_CONFIG_FILES) if (existsSync(join(root, file))) return false;
|
|
802
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
803
|
+
if (EMPTY_PROJECT_ALLOWED_ENTRIES.has(entry.name)) continue;
|
|
804
|
+
if (entry.name.startsWith("README")) continue;
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
const packageJsonPath = join(root, "package.json");
|
|
808
|
+
if (!existsSync(packageJsonPath)) return true;
|
|
809
|
+
let pkgRaw;
|
|
810
|
+
try {
|
|
811
|
+
pkgRaw = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
812
|
+
} catch {
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
const pkg = asRecord(pkgRaw);
|
|
816
|
+
if (!pkg) return false;
|
|
817
|
+
const deps = {
|
|
818
|
+
...asRecord(pkg.dependencies) ?? {},
|
|
819
|
+
...asRecord(pkg.devDependencies) ?? {}
|
|
820
|
+
};
|
|
821
|
+
if (Object.keys(deps).some((dep) => !EMPTY_PROJECT_ALLOWED_PACKAGES.has(dep))) return false;
|
|
822
|
+
const scripts = asRecord(pkg.scripts) ?? {};
|
|
823
|
+
if (Object.keys(scripts).some((name) => name !== "test")) return false;
|
|
824
|
+
return true;
|
|
825
|
+
}
|
|
826
|
+
function parsePackageManagerField(pkg) {
|
|
827
|
+
if (!pkg || typeof pkg !== "object") return;
|
|
828
|
+
const raw = pkg.packageManager;
|
|
829
|
+
if (typeof raw !== "string") return;
|
|
830
|
+
const name = raw.split("@", 1)[0]?.trim();
|
|
831
|
+
if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") return name;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Returns `true` when a project should be treated as pnpm-managed for
|
|
835
|
+
* purposes of installing build-script allowlists and picking a
|
|
836
|
+
* `workspace:` rewrite strategy. We honor BOTH local signals — a committed
|
|
837
|
+
* `pnpm-lock.yaml`, a local `pnpm-workspace.yaml`, or a Corepack
|
|
838
|
+
* `packageManager: pnpm` pin in `package.json` — AND the same signals on
|
|
839
|
+
* any ancestor workspace root.
|
|
840
|
+
*
|
|
841
|
+
* Ancestor walking matters for pnpm workspace subpackages: pnpm writes the
|
|
842
|
+
* lockfile at the workspace root, not per-package, and many subpackages
|
|
843
|
+
* omit their own `packageManager` field in favor of the workspace root's.
|
|
844
|
+
* A subpackage with a `workspace:*` dep in its `package.json` but no local
|
|
845
|
+
* pnpm markers is still pnpm-managed if the enclosing workspace root has
|
|
846
|
+
* `pnpm-workspace.yaml`, a `pnpm-lock.yaml`, or `packageManager: pnpm@...`.
|
|
847
|
+
*
|
|
848
|
+
* Round 33: local non-pnpm signals MUST outrank the ancestor walk. A bun
|
|
849
|
+
* or yarn subproject nested under a pnpm workspace ancestor is still
|
|
850
|
+
* bun/yarn — its local `bun.lockb` / `yarn.lock` / `package-lock.json` or
|
|
851
|
+
* a local Corepack pin for bun/yarn/npm proves the ancestor's pnpm shape
|
|
852
|
+
* does not govern this subpackage. `detectProjectState` would pick
|
|
853
|
+
* bun/yarn for such a project and try to install with that manager, so
|
|
854
|
+
* the pnpm check must not inherit ancestor workspace state too eagerly.
|
|
855
|
+
*/
|
|
856
|
+
function isPnpmProject(root, pkg) {
|
|
857
|
+
if (existsSync(join(root, "pnpm-lock.yaml"))) return true;
|
|
858
|
+
if (existsSync(join(root, "pnpm-workspace.yaml"))) return true;
|
|
859
|
+
const localPm = parsePackageManagerField(pkg);
|
|
860
|
+
if (localPm === "pnpm") return true;
|
|
861
|
+
if (localPm === "bun" || localPm === "yarn" || localPm === "npm") return false;
|
|
862
|
+
if (existsSync(join(root, "bun.lockb")) || existsSync(join(root, "bun.lock")) || existsSync(join(root, "yarn.lock")) || existsSync(join(root, "package-lock.json"))) return false;
|
|
863
|
+
return hasAncestorPnpmMarkers(root);
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Returns `true` when `start` is a declared member of an ancestor pnpm
|
|
867
|
+
* workspace. Used to detect pnpm workspace subpackages whose enclosing
|
|
868
|
+
* workspace root is pnpm-managed even when the subpackage has no local
|
|
869
|
+
* pnpm signals.
|
|
870
|
+
*
|
|
871
|
+
* Round 43: an ancestor `pnpm-workspace.yaml` / `pnpm-lock.yaml` /
|
|
872
|
+
* Corepack pnpm pin alone is NOT proof that `start` is governed by
|
|
873
|
+
* that repo's pnpm install. A scratch app nested under a pnpm
|
|
874
|
+
* monorepo is a separate project when its path doesn't match the
|
|
875
|
+
* workspace globs. Previously this returned `true` too eagerly, and
|
|
876
|
+
* `ensureVoidDependency` / `scaffoldPagesApp` then treated it like a
|
|
877
|
+
* workspace member even though the subsequent local install used the
|
|
878
|
+
* standalone fallback path.
|
|
879
|
+
*
|
|
880
|
+
* Delegate workspace membership to `findWorkspaceRoot` +
|
|
881
|
+
* `isWorkspaceSubpackage` — the same check used by the
|
|
882
|
+
* workspace-subpackage skip path in `runInitCommand` — so the two
|
|
883
|
+
* paths can't disagree.
|
|
884
|
+
*/
|
|
885
|
+
function hasAncestorPnpmMarkers(start) {
|
|
886
|
+
const workspaceRoot = findWorkspaceRoot(start);
|
|
887
|
+
if (workspaceRoot === void 0) return false;
|
|
888
|
+
if (!isWorkspaceSubpackage(workspaceRoot, start)) return false;
|
|
889
|
+
if (existsSync(join(workspaceRoot, "pnpm-workspace.yaml"))) return true;
|
|
890
|
+
if (existsSync(join(workspaceRoot, "pnpm-lock.yaml"))) return true;
|
|
891
|
+
const pkgPath = join(workspaceRoot, "package.json");
|
|
892
|
+
if (existsSync(pkgPath)) try {
|
|
893
|
+
if (parsePackageManagerField(JSON.parse(readFileSync(pkgPath, "utf-8"))) === "pnpm") return true;
|
|
894
|
+
} catch {}
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Walk up from `start` checking each ancestor directory for a
|
|
899
|
+
* `node_modules/void/package.json`. Mirrors Node's module resolution
|
|
900
|
+
* algorithm for the `void` package name — returns true as soon as any
|
|
901
|
+
* ancestor has `void` installed, false only when the entire chain is
|
|
902
|
+
* empty. Used by the demo install gate so workspace subpackages whose
|
|
903
|
+
* `void` is hoisted at the workspace root are detected as already
|
|
904
|
+
* installed and we don't trigger a spurious subpackage install.
|
|
905
|
+
*
|
|
906
|
+
* PnP rationale: in a Yarn Berry PnP workspace, dependencies resolve
|
|
907
|
+
* via `.pnp.cjs` at the workspace root and there is no `node_modules`
|
|
908
|
+
* tree — not in the subpackage, not in ancestors. By the time we call
|
|
909
|
+
* this, `scaffoldDemo`/`ensureVoidDependency` has added `void` to the
|
|
910
|
+
* subpackage's `package.json` (or verified it was already there), and
|
|
911
|
+
* under PnP the workspace-root install is what wires up resolution.
|
|
912
|
+
*
|
|
913
|
+
* Round 40: a committed-but-unfetched `.pnp.cjs` is NOT proof that
|
|
914
|
+
* `yarn install` has run. Yarn Berry repos commonly commit `.pnp.cjs`
|
|
915
|
+
* but gitignore `.yarn/cache/*.zip` (zero-install is opt-in), so a
|
|
916
|
+
* fresh clone has the runtime file on disk while the archives it
|
|
917
|
+
* points at are missing. The stricter `findInstalledYarnPnpRuntime`
|
|
918
|
+
* also requires `<pnp-root>/.yarn/install-state.gz` — Yarn's canonical
|
|
919
|
+
* post-install marker, gitignored by convention — as proof that yarn
|
|
920
|
+
* install has actually populated the archives. Without that marker,
|
|
921
|
+
* we fall through and return false so the caller triggers the install
|
|
922
|
+
* step before attempting to spawn drizzle-kit.
|
|
923
|
+
*/
|
|
924
|
+
function isVoidResolvableFrom(start) {
|
|
925
|
+
let dir = start;
|
|
926
|
+
while (true) {
|
|
927
|
+
if (existsSync(join(dir, "node_modules", "void", "package.json"))) return true;
|
|
928
|
+
const parent = dirname(dir);
|
|
929
|
+
if (parent === dir) break;
|
|
930
|
+
dir = parent;
|
|
931
|
+
}
|
|
932
|
+
const installed = findInstalledYarnPnpRuntime(start);
|
|
933
|
+
if (installed !== void 0) {
|
|
934
|
+
const probeArgs = installed.pnpCjs ? ["-e", "require.resolve('void')"] : [
|
|
935
|
+
"--input-type=module",
|
|
936
|
+
"-e",
|
|
937
|
+
"import.meta.resolve('void')"
|
|
938
|
+
];
|
|
939
|
+
const probe = spawnSync(process.execPath, probeArgs, {
|
|
940
|
+
cwd: start,
|
|
941
|
+
stdio: "ignore",
|
|
942
|
+
shell: process.platform === "win32",
|
|
943
|
+
env: buildDrizzleKitSpawnEnv(start)
|
|
944
|
+
});
|
|
945
|
+
return !probe.error && probe.status === 0;
|
|
946
|
+
}
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Walks ancestors from `start` looking for workspace-root markers: a
|
|
951
|
+
* `pnpm-workspace.yaml` next to a `package.json`, or a `package.json`
|
|
952
|
+
* whose top-level `workspaces` field exists (array or object). Returns
|
|
953
|
+
* the discovered workspace root path if `start` is a subpackage inside
|
|
954
|
+
* such a workspace, or `undefined` when it is not. Stops at the
|
|
955
|
+
* filesystem root. Skips `start` itself — a root with its own
|
|
956
|
+
* `workspaces` field is its own root, not a subpackage.
|
|
957
|
+
*/
|
|
958
|
+
function findWorkspaceRoot(start) {
|
|
959
|
+
let dir = dirname(start);
|
|
960
|
+
while (true) {
|
|
961
|
+
const pkgPath = join(dir, "package.json");
|
|
962
|
+
if (existsSync(pkgPath)) try {
|
|
963
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
964
|
+
if (pkg && typeof pkg === "object" && "workspaces" in pkg) return dir;
|
|
965
|
+
} catch {}
|
|
966
|
+
if (existsSync(join(dir, "pnpm-workspace.yaml"))) return dir;
|
|
967
|
+
const parent = dirname(dir);
|
|
968
|
+
if (parent === dir) return;
|
|
969
|
+
dir = parent;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Read the declared workspace-member glob patterns from a workspace root.
|
|
974
|
+
* Supports:
|
|
975
|
+
* - `pnpm-workspace.yaml` — extracts patterns under the top-level
|
|
976
|
+
* `packages:` key via a regex matcher (no `js-yaml` dep). Only the
|
|
977
|
+
* `packages:` key is used; pnpm-specific keys like `catalog:` are
|
|
978
|
+
* ignored.
|
|
979
|
+
* - `package.json#workspaces` — either an array of patterns or a
|
|
980
|
+
* `{ packages: [...] }` object form (npm/yarn/bun convention).
|
|
981
|
+
*
|
|
982
|
+
* Returns an empty array if no patterns are declared or parsing fails —
|
|
983
|
+
* callers should treat that as "can't verify membership" and fall back
|
|
984
|
+
* to the fresh-clone install path.
|
|
985
|
+
*/
|
|
986
|
+
function readWorkspacePatterns(workspaceRoot) {
|
|
987
|
+
const pnpmWorkspacePath = join(workspaceRoot, "pnpm-workspace.yaml");
|
|
988
|
+
if (existsSync(pnpmWorkspacePath)) try {
|
|
989
|
+
return extractPnpmWorkspacePackages(readFileSync(pnpmWorkspacePath, "utf-8"));
|
|
990
|
+
} catch {
|
|
991
|
+
return [];
|
|
992
|
+
}
|
|
993
|
+
const pkgPath = join(workspaceRoot, "package.json");
|
|
994
|
+
if (existsSync(pkgPath)) try {
|
|
995
|
+
const workspaces = JSON.parse(readFileSync(pkgPath, "utf-8")).workspaces;
|
|
996
|
+
if (Array.isArray(workspaces)) return workspaces.filter((x) => typeof x === "string");
|
|
997
|
+
if (workspaces && typeof workspaces === "object") {
|
|
998
|
+
const packages = workspaces.packages;
|
|
999
|
+
if (Array.isArray(packages)) return packages.filter((x) => typeof x === "string");
|
|
1000
|
+
}
|
|
1001
|
+
} catch {
|
|
1002
|
+
return [];
|
|
1003
|
+
}
|
|
1004
|
+
return [];
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Strip a trailing YAML line comment from a raw line while preserving
|
|
1008
|
+
* `#` characters that live inside single- or double-quoted scalars.
|
|
1009
|
+
* YAML treats `#` as a comment marker only when it is preceded by
|
|
1010
|
+
* whitespace (or is the first column) AND it is not inside a quoted
|
|
1011
|
+
* scalar. Flow-style inline quoting is handled by a plain state machine
|
|
1012
|
+
* — no escape sequence support, because real-world workspace globs
|
|
1013
|
+
* never contain escaped quotes.
|
|
1014
|
+
*
|
|
1015
|
+
* This replaces the original `/\s+#.*$/` strip which was greedy and
|
|
1016
|
+
* would truncate legitimate quoted values like `- "path #hash/*"`,
|
|
1017
|
+
* leaving a broken glob that silently failed
|
|
1018
|
+
* `isWorkspaceSubpackage` membership.
|
|
1019
|
+
*/
|
|
1020
|
+
function stripYamlLineComment(line) {
|
|
1021
|
+
let inSingle = false;
|
|
1022
|
+
let inDouble = false;
|
|
1023
|
+
for (let i = 0; i < line.length; i++) {
|
|
1024
|
+
const ch = line[i];
|
|
1025
|
+
if (inSingle) {
|
|
1026
|
+
if (ch === "'") inSingle = false;
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
if (inDouble) {
|
|
1030
|
+
if (ch === "\"") inDouble = false;
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
if (ch === "'") {
|
|
1034
|
+
inSingle = true;
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
if (ch === "\"") {
|
|
1038
|
+
inDouble = true;
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
if (ch === "#" && i > 0 && /\s/.test(line[i - 1])) return line.slice(0, i).replace(/\s+$/, "");
|
|
1042
|
+
}
|
|
1043
|
+
return line;
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Minimal YAML extractor for the `packages:` key of a
|
|
1047
|
+
* `pnpm-workspace.yaml`. Supports both the block-style form:
|
|
1048
|
+
*
|
|
1049
|
+
* packages:
|
|
1050
|
+
* - 'packages/*'
|
|
1051
|
+
* - "apps/*"
|
|
1052
|
+
* - docs
|
|
1053
|
+
*
|
|
1054
|
+
* and the flow-style (inline) form:
|
|
1055
|
+
*
|
|
1056
|
+
* packages: ['packages/*', "apps/*"]
|
|
1057
|
+
* packages: [packages/*]
|
|
1058
|
+
*
|
|
1059
|
+
* Anything else (other top-level keys, comments, nested blocks) is
|
|
1060
|
+
* ignored. This intentionally does NOT pull in `js-yaml` — the
|
|
1061
|
+
* surface area we need is tiny and the real YAML we see in practice
|
|
1062
|
+
* uses one of these two shapes.
|
|
1063
|
+
*/
|
|
1064
|
+
function extractPnpmWorkspacePackages(yaml) {
|
|
1065
|
+
const lines = yaml.split(/\r?\n/);
|
|
1066
|
+
const patterns = [];
|
|
1067
|
+
let inPackages = false;
|
|
1068
|
+
for (const rawLine of lines) {
|
|
1069
|
+
const line = stripYamlLineComment(rawLine);
|
|
1070
|
+
const inlineMatch = line.match(/^\s*packages\s*:\s*\[(.*)\]\s*$/);
|
|
1071
|
+
if (inlineMatch) {
|
|
1072
|
+
const items = inlineMatch[1].match(/(['"])(?:(?!\1).)*\1|[^,\s]+/g) ?? [];
|
|
1073
|
+
for (const raw of items) {
|
|
1074
|
+
const unquoted = raw.trim().replace(/^(['"])(.*)\1$/, "$2");
|
|
1075
|
+
if (unquoted) patterns.push(unquoted);
|
|
1076
|
+
}
|
|
1077
|
+
inPackages = false;
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
if (/^\s*packages\s*:\s*$/.test(line)) {
|
|
1081
|
+
inPackages = true;
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
if (!inPackages) continue;
|
|
1085
|
+
if (/^[A-Za-z_][\w-]*\s*:\s*(?:\S.*)?$/.test(line) && !/^\s/.test(line)) {
|
|
1086
|
+
inPackages = false;
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
const m = line.match(/^\s*-\s*(['"]?)([^'"\s][^'"]*?)\1\s*$/);
|
|
1090
|
+
if (m) patterns.push(m[2]);
|
|
1091
|
+
}
|
|
1092
|
+
return patterns;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Expand brace groups in a workspace glob pattern. Supports the forms
|
|
1096
|
+
* that appear in real workspace manifests, like `{apps,packages}/*`
|
|
1097
|
+
* and nested forms like `{apps,packages}/{core,web}`. Returns an array
|
|
1098
|
+
* of expanded patterns with no braces. No-op for patterns that don't
|
|
1099
|
+
* contain `{...}`. Does not support escaped braces or nested braces
|
|
1100
|
+
* inside a group (neither appears in real workspace manifests).
|
|
1101
|
+
*/
|
|
1102
|
+
function expandBraces(pattern) {
|
|
1103
|
+
const match = pattern.match(/^([^{}]*)\{([^{}]*)\}(.*)$/);
|
|
1104
|
+
if (!match) return [pattern];
|
|
1105
|
+
const [, before, group, after] = match;
|
|
1106
|
+
const options = group.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1107
|
+
if (options.length === 0) return [pattern];
|
|
1108
|
+
const results = [];
|
|
1109
|
+
for (const opt of options) for (const sub of expandBraces(before + opt + after)) results.push(sub);
|
|
1110
|
+
return results;
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Match a path against a workspace glob pattern. Supports the forms
|
|
1114
|
+
* that appear in real workspace manifests:
|
|
1115
|
+
* - Literal path: `packages/void` → exact match.
|
|
1116
|
+
* - Leading `./`: `./packages/*` → stripped before matching.
|
|
1117
|
+
* - Single-segment glob: `packages/*` → one segment under prefix.
|
|
1118
|
+
* - Partial-segment glob: `packages/*-app` → regex within a segment.
|
|
1119
|
+
* - Recursive glob: `packages/**` → any depth under prefix.
|
|
1120
|
+
* - Brace expansion: `{apps,packages}/*` → expanded before matching.
|
|
1121
|
+
* Does not support character classes or extended globs — those don't
|
|
1122
|
+
* appear in any workspace manifest we've seen, and adding a full
|
|
1123
|
+
* minimatch pulls in a dep we don't need.
|
|
1124
|
+
*/
|
|
1125
|
+
function matchesWorkspacePattern(relPath, pattern) {
|
|
1126
|
+
return expandBraces(pattern).some((p) => matchesSingleWorkspacePattern(relPath, p));
|
|
1127
|
+
}
|
|
1128
|
+
function matchesSingleWorkspacePattern(relPath, pattern) {
|
|
1129
|
+
const normalizedPattern = pattern.replace(/^\.\/+/, "");
|
|
1130
|
+
return matchSegments(relPath.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean), 0, normalizedPattern.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean).map(compilePatternSegment), 0);
|
|
1131
|
+
}
|
|
1132
|
+
function compilePatternSegment(seg) {
|
|
1133
|
+
if (seg === "**") return { kind: "globstar" };
|
|
1134
|
+
if (!seg.includes("*")) return {
|
|
1135
|
+
kind: "literal",
|
|
1136
|
+
value: seg
|
|
1137
|
+
};
|
|
1138
|
+
return {
|
|
1139
|
+
kind: "regex",
|
|
1140
|
+
re: new RegExp("^" + seg.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*") + "$")
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
function matchSegments(path, pi, pat, qi) {
|
|
1144
|
+
while (qi < pat.length) {
|
|
1145
|
+
const seg = pat[qi];
|
|
1146
|
+
if (seg.kind === "globstar") {
|
|
1147
|
+
if (qi === pat.length - 1) return true;
|
|
1148
|
+
for (let k = pi; k <= path.length; k++) if (matchSegments(path, k, pat, qi + 1)) return true;
|
|
1149
|
+
return false;
|
|
1150
|
+
}
|
|
1151
|
+
if (pi >= path.length) return false;
|
|
1152
|
+
if (seg.kind === "literal") {
|
|
1153
|
+
if (seg.value !== path[pi]) return false;
|
|
1154
|
+
} else if (!seg.re.test(path[pi])) return false;
|
|
1155
|
+
pi++;
|
|
1156
|
+
qi++;
|
|
1157
|
+
}
|
|
1158
|
+
return pi === path.length;
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Verify that `root` is actually a declared member of the workspace
|
|
1162
|
+
* rooted at `workspaceRoot`. An ancestor workspace root found by
|
|
1163
|
+
* `findWorkspaceRoot` is not enough — a standalone app nested under a
|
|
1164
|
+
* monorepo (e.g. `repo/scratch/my-app` inside a repo whose
|
|
1165
|
+
* `pnpm-workspace.yaml` only lists `packages/*`) is NOT a workspace
|
|
1166
|
+
* subpackage, and treating it as one leaves the demo scaffold in a
|
|
1167
|
+
* broken state: install is skipped, `generateStarterMigration` is
|
|
1168
|
+
* skipped, and the user ends up with unresolved `void` imports.
|
|
1169
|
+
*
|
|
1170
|
+
* Supports positive and negation (`!pattern`) patterns. A path is
|
|
1171
|
+
* considered a member if it matches at least one positive pattern and
|
|
1172
|
+
* no negation pattern.
|
|
1173
|
+
*
|
|
1174
|
+
* Returns `false` if no patterns are declared at all — callers treat
|
|
1175
|
+
* that as "not a subpackage" so the fresh-clone install path still runs.
|
|
1176
|
+
*/
|
|
1177
|
+
function isWorkspaceSubpackage(workspaceRoot, root) {
|
|
1178
|
+
const relPath = relative(workspaceRoot, root);
|
|
1179
|
+
if (relPath === "" || relPath.startsWith("..")) return false;
|
|
1180
|
+
const normalized = relPath.split(/[\\/]/).join("/");
|
|
1181
|
+
const patterns = readWorkspacePatterns(workspaceRoot);
|
|
1182
|
+
if (patterns.length === 0) return false;
|
|
1183
|
+
let matched = false;
|
|
1184
|
+
for (const raw of patterns) {
|
|
1185
|
+
const isNegation = raw.startsWith("!");
|
|
1186
|
+
if (matchesWorkspacePattern(normalized, isNegation ? raw.slice(1) : raw)) {
|
|
1187
|
+
if (isNegation) return false;
|
|
1188
|
+
matched = true;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return matched;
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Returns true iff `root` is structurally a declared member of an
|
|
1195
|
+
* ancestor workspace — regardless of whether the subpackage manages its
|
|
1196
|
+
* own install via a local lockfile or Corepack pin. This is the signal
|
|
1197
|
+
* Step 8 (GitHub Actions) uses: `.github/workflows/*` files are only
|
|
1198
|
+
* read from the repository root, so dropping them under a subpackage
|
|
1199
|
+
* root is always wrong, whether or not the subpackage self-manages
|
|
1200
|
+
* install.
|
|
1201
|
+
*/
|
|
1202
|
+
function isInsideWorkspaceSubpackage(root) {
|
|
1203
|
+
const workspaceRoot = findWorkspaceRoot(root);
|
|
1204
|
+
if (workspaceRoot === void 0) return false;
|
|
1205
|
+
return isWorkspaceSubpackage(workspaceRoot, root);
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Returns the workspace root if `root` should defer its install +
|
|
1209
|
+
* starter migration to a parent workspace install — i.e. it is a
|
|
1210
|
+
* declared member of an ancestor workspace AND has no local lockfile or
|
|
1211
|
+
* non-pnpm Corepack pin claiming it manages its own install. Returns
|
|
1212
|
+
* undefined when `root` is either not a subpackage at all, or is a
|
|
1213
|
+
* leaf-managed project nested inside a workspace.
|
|
1214
|
+
*
|
|
1215
|
+
* This is the single source of truth used by both the starter and the
|
|
1216
|
+
* demo branches in `runInitCommand` so they can never disagree about
|
|
1217
|
+
* whether to skip install/migration.
|
|
1218
|
+
*/
|
|
1219
|
+
function resolveWorkspaceDeferral(root, pkg) {
|
|
1220
|
+
const workspaceRoot = findWorkspaceRoot(root);
|
|
1221
|
+
if (workspaceRoot === void 0) return;
|
|
1222
|
+
if (!isWorkspaceSubpackage(workspaceRoot, root)) return;
|
|
1223
|
+
if (hasLocalLockfile(root)) return;
|
|
1224
|
+
const localPm = parsePackageManagerField(pkg);
|
|
1225
|
+
if (localPm === "bun" || localPm === "yarn" || localPm === "npm") return;
|
|
1226
|
+
return { workspaceRoot };
|
|
1227
|
+
}
|
|
1228
|
+
function detectProjectState(root) {
|
|
1229
|
+
let hasTypescript = false;
|
|
1230
|
+
let pkg;
|
|
1231
|
+
try {
|
|
1232
|
+
pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
|
|
1233
|
+
const pkgRecord = pkg ?? {};
|
|
1234
|
+
const allDeps = {
|
|
1235
|
+
...asRecord(pkgRecord.dependencies) ?? {},
|
|
1236
|
+
...asRecord(pkgRecord.devDependencies) ?? {}
|
|
1237
|
+
};
|
|
1238
|
+
hasTypescript = "typescript" in allDeps || "@typescript/native-preview" in allDeps;
|
|
1239
|
+
} catch {}
|
|
1240
|
+
let packageManager;
|
|
1241
|
+
if (existsSync(join(root, "pnpm-lock.yaml"))) packageManager = "pnpm";
|
|
1242
|
+
else if (existsSync(join(root, "yarn.lock"))) packageManager = "yarn";
|
|
1243
|
+
else if (existsSync(join(root, "bun.lock")) || existsSync(join(root, "bun.lockb"))) packageManager = "bun";
|
|
1244
|
+
else if (existsSync(join(root, "pnpm-workspace.yaml"))) packageManager = "pnpm";
|
|
1245
|
+
else {
|
|
1246
|
+
const field = parsePackageManagerField(pkg);
|
|
1247
|
+
if (field === "pnpm" || field === "yarn" || field === "bun") packageManager = field;
|
|
1248
|
+
else packageManager = "npm";
|
|
1249
|
+
}
|
|
1250
|
+
return {
|
|
1251
|
+
hasTypescript,
|
|
1252
|
+
hasTsconfig: existsSync(join(root, "tsconfig.json")),
|
|
1253
|
+
hasGithubWorkflows: existsSync(join(root, ".github", "workflows")),
|
|
1254
|
+
hasRoutes: existsSync(join(root, "routes")),
|
|
1255
|
+
hasMigrations: existsSync(join(root, "db", "migrations")) || existsSync(join(root, "migrations")),
|
|
1256
|
+
hasPages: existsSync(join(root, "pages")),
|
|
1257
|
+
packageManager,
|
|
1258
|
+
typescriptWanted: hasTypescript
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
function findViteConfig(root) {
|
|
1262
|
+
return VITE_CONFIG_FILES.find((file) => existsSync(join(root, file)));
|
|
1263
|
+
}
|
|
1264
|
+
function hasDrizzleSchema(root) {
|
|
1265
|
+
return existsSync(join(root, "db", "schema.ts")) || existsSync(join(root, "db", "schema"));
|
|
1266
|
+
}
|
|
1267
|
+
function detectPagesFramework(root) {
|
|
1268
|
+
try {
|
|
1269
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
|
|
1270
|
+
return detectScaffoldFrameworkFromDependencies({
|
|
1271
|
+
...asRecord(pkg.dependencies) ?? {},
|
|
1272
|
+
...asRecord(pkg.devDependencies) ?? {}
|
|
1273
|
+
});
|
|
1274
|
+
} catch {
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
function readPackageJson(root) {
|
|
1279
|
+
try {
|
|
1280
|
+
return asRecord(JSON.parse(readFileSync(join(root, "package.json"), "utf-8"))) ?? void 0;
|
|
1281
|
+
} catch {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
const DEPENDENCY_FIELDS = [
|
|
1286
|
+
"dependencies",
|
|
1287
|
+
"devDependencies",
|
|
1288
|
+
"peerDependencies",
|
|
1289
|
+
"optionalDependencies"
|
|
1290
|
+
];
|
|
1291
|
+
function getDeclaredDependencySpec(root, packageName) {
|
|
1292
|
+
const pkg = readPackageJson(root);
|
|
1293
|
+
if (!pkg) return;
|
|
1294
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
1295
|
+
const group = asRecord(pkg[field]);
|
|
1296
|
+
if (typeof group?.[packageName] === "string") return group[packageName];
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
function resolveScaffoldDependencySpec(root, packageName, fallbackVersion) {
|
|
1300
|
+
const explicit = getDeclaredDependencySpec(root, packageName);
|
|
1301
|
+
if (explicit) return explicit;
|
|
1302
|
+
return fallbackVersion;
|
|
1303
|
+
}
|
|
1304
|
+
function setupTsconfig(root, state, pagesFramework) {
|
|
1305
|
+
const tsconfigPath = join(root, "tsconfig.json");
|
|
1306
|
+
if (state.hasTsconfig) {
|
|
1307
|
+
let tsconfig;
|
|
1308
|
+
try {
|
|
1309
|
+
const parsed = parseJsoncFile(tsconfigPath);
|
|
1310
|
+
if (!parsed) throw new SyntaxError("Failed to parse tsconfig.json");
|
|
1311
|
+
tsconfig = parsed;
|
|
1312
|
+
} catch {
|
|
1313
|
+
R.warn("Could not parse tsconfig.json, skipping");
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
let updated = false;
|
|
1317
|
+
const rootPathBaseDir = effectiveConfigPathBaseDir(tsconfig, tsconfigPath);
|
|
1318
|
+
const existingPatch = collectTsconfigPatchFromConfig(tsconfig, tsconfigPath, root, rootPathBaseDir, /* @__PURE__ */ new Set());
|
|
1319
|
+
const voidPatch = createVoidTsconfigPatch(root, rootPathBaseDir);
|
|
1320
|
+
const extendsValue = tsconfig.extends;
|
|
1321
|
+
const target = "./.void/tsconfig.json";
|
|
1322
|
+
if (!(extendsValue === target || Array.isArray(extendsValue) && extendsValue.includes(target))) {
|
|
1323
|
+
if (typeof extendsValue === "string") tsconfig.extends = [extendsValue, target];
|
|
1324
|
+
else if (Array.isArray(extendsValue)) tsconfig.extends = [...extendsValue, target];
|
|
1325
|
+
else tsconfig.extends = target;
|
|
1326
|
+
updated = true;
|
|
1327
|
+
R.success("Updated tsconfig.json — added extends .void/tsconfig.json");
|
|
1328
|
+
}
|
|
1329
|
+
const compilerOptions = asRecord(tsconfig.compilerOptions) ?? {};
|
|
1330
|
+
tsconfig.compilerOptions = compilerOptions;
|
|
1331
|
+
const types = Array.isArray(compilerOptions.types) ? compilerOptions.types.filter((value) => typeof value === "string") : [];
|
|
1332
|
+
if (!types.includes("void/env")) {
|
|
1333
|
+
types.push("void/env");
|
|
1334
|
+
updated = true;
|
|
1335
|
+
R.success("Updated tsconfig.json — added void/env to types");
|
|
1336
|
+
}
|
|
1337
|
+
const cfIdx = types.indexOf("@cloudflare/workers-types");
|
|
1338
|
+
if (cfIdx !== -1) {
|
|
1339
|
+
types.splice(cfIdx, 1);
|
|
1340
|
+
updated = true;
|
|
1341
|
+
R.success("Removed @cloudflare/workers-types from tsconfig types (included in void/env)");
|
|
1342
|
+
}
|
|
1343
|
+
compilerOptions.types = types;
|
|
1344
|
+
const existingFiles = uniqueStrings(existingPatch.files);
|
|
1345
|
+
if (existingFiles.length > 0 || Array.isArray(tsconfig.files)) {
|
|
1346
|
+
const files = uniqueStrings([...existingFiles, ...voidPatch.files]);
|
|
1347
|
+
if (JSON.stringify(tsconfig.files) !== JSON.stringify(files)) {
|
|
1348
|
+
tsconfig.files = files;
|
|
1349
|
+
updated = true;
|
|
1350
|
+
R.success("Updated tsconfig.json — merged generated Void type files");
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
if (Object.keys(existingPatch.paths).length > 0 || asRecord(compilerOptions.paths) !== null) {
|
|
1354
|
+
const paths = { ...existingPatch.paths };
|
|
1355
|
+
mergePathMaps(paths, voidPatch.paths);
|
|
1356
|
+
const existingPaths = readPaths(compilerOptions.paths);
|
|
1357
|
+
if (JSON.stringify(existingPaths) !== JSON.stringify(paths)) {
|
|
1358
|
+
compilerOptions.paths = paths;
|
|
1359
|
+
updated = true;
|
|
1360
|
+
R.success("Updated tsconfig.json — merged generated Void path aliases");
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (updated) {
|
|
1364
|
+
const { extends: ext, ...rest } = tsconfig;
|
|
1365
|
+
const ordered = {
|
|
1366
|
+
extends: ext,
|
|
1367
|
+
...rest
|
|
1368
|
+
};
|
|
1369
|
+
writeFileSync(tsconfigPath, JSON.stringify(ordered, null, 2) + "\n");
|
|
1370
|
+
} else R.success("tsconfig.json looks good");
|
|
1371
|
+
try {
|
|
1372
|
+
const pkgPath = join(root, "package.json");
|
|
1373
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
1374
|
+
let pkgUpdated = false;
|
|
1375
|
+
if (pkg.dependencies?.["@cloudflare/workers-types"]) {
|
|
1376
|
+
delete pkg.dependencies["@cloudflare/workers-types"];
|
|
1377
|
+
pkgUpdated = true;
|
|
1378
|
+
}
|
|
1379
|
+
if (pkg.devDependencies?.["@cloudflare/workers-types"]) {
|
|
1380
|
+
delete pkg.devDependencies["@cloudflare/workers-types"];
|
|
1381
|
+
pkgUpdated = true;
|
|
1382
|
+
}
|
|
1383
|
+
if (pkgUpdated) {
|
|
1384
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
1385
|
+
R.success("Removed @cloudflare/workers-types from package.json (included in void/env)");
|
|
1386
|
+
R.info(`Re-run ${state.packageManager} install to update your lockfile`);
|
|
1387
|
+
}
|
|
1388
|
+
} catch {}
|
|
1389
|
+
} else {
|
|
1390
|
+
const tsconfig = {
|
|
1391
|
+
extends: "./.void/tsconfig.json",
|
|
1392
|
+
compilerOptions: {
|
|
1393
|
+
target: "ES2022",
|
|
1394
|
+
module: "ESNext",
|
|
1395
|
+
moduleResolution: "bundler",
|
|
1396
|
+
strict: true,
|
|
1397
|
+
esModuleInterop: true,
|
|
1398
|
+
skipLibCheck: true,
|
|
1399
|
+
types: ["void/env"],
|
|
1400
|
+
...getScaffoldTsconfigCompilerOptions(pagesFramework)
|
|
1401
|
+
},
|
|
1402
|
+
include: [
|
|
1403
|
+
"src",
|
|
1404
|
+
"pages",
|
|
1405
|
+
"routes",
|
|
1406
|
+
"middleware",
|
|
1407
|
+
"crons",
|
|
1408
|
+
"queues",
|
|
1409
|
+
"db"
|
|
1410
|
+
]
|
|
1411
|
+
};
|
|
1412
|
+
writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
|
|
1413
|
+
R.success("Created tsconfig.json");
|
|
1414
|
+
if (!state.hasTypescript) R.info("Don't forget to install typescript");
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
/** Today's UTC date as YYYY-MM-DD for wrangler compatibility_date. */
|
|
1418
|
+
function getTodayCompatDate() {
|
|
1419
|
+
const d = /* @__PURE__ */ new Date();
|
|
1420
|
+
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
|
|
1421
|
+
}
|
|
1422
|
+
function readProjectWranglerCompatDate(root) {
|
|
1423
|
+
for (const name of ["wrangler.jsonc", "wrangler.json"]) {
|
|
1424
|
+
const configPath = join(root, name);
|
|
1425
|
+
if (!existsSync(configPath)) continue;
|
|
1426
|
+
try {
|
|
1427
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
1428
|
+
const errors = [];
|
|
1429
|
+
const parsed = parse(raw, errors);
|
|
1430
|
+
if (errors.length > 0) throw new SyntaxError("Failed to parse JSONC");
|
|
1431
|
+
if (typeof parsed?.compatibility_date === "string") return parsed.compatibility_date;
|
|
1432
|
+
} catch {}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Ensure wrangler.jsonc exists with `compatibility_date` and `nodejs_als` flag
|
|
1437
|
+
* for class B/C meta-frameworks that need it for local dev.
|
|
1438
|
+
*/
|
|
1439
|
+
function setupWranglerConfig(root, framework) {
|
|
1440
|
+
if (framework.class === "a") return;
|
|
1441
|
+
let configPath;
|
|
1442
|
+
let parsed;
|
|
1443
|
+
let created = false;
|
|
1444
|
+
for (const name of ["wrangler.jsonc", "wrangler.json"]) {
|
|
1445
|
+
const p = join(root, name);
|
|
1446
|
+
if (existsSync(p)) {
|
|
1447
|
+
configPath = p;
|
|
1448
|
+
try {
|
|
1449
|
+
const raw = readFileSync(p, "utf-8");
|
|
1450
|
+
const errors = [];
|
|
1451
|
+
parsed = parse(raw, errors);
|
|
1452
|
+
if (errors.length > 0) throw new SyntaxError("Failed to parse JSONC");
|
|
1453
|
+
} catch {}
|
|
1454
|
+
break;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (!configPath || !parsed) {
|
|
1458
|
+
configPath = join(root, "wrangler.jsonc");
|
|
1459
|
+
parsed = {};
|
|
1460
|
+
created = true;
|
|
1461
|
+
}
|
|
1462
|
+
let modified = false;
|
|
1463
|
+
if (!parsed.compatibility_date) {
|
|
1464
|
+
parsed.compatibility_date = getTodayCompatDate();
|
|
1465
|
+
modified = true;
|
|
1466
|
+
}
|
|
1467
|
+
const flags = Array.isArray(parsed.compatibility_flags) ? parsed.compatibility_flags : [];
|
|
1468
|
+
if (!flags.includes("nodejs_als")) {
|
|
1469
|
+
parsed.compatibility_flags = [...flags, "nodejs_als"];
|
|
1470
|
+
modified = true;
|
|
1471
|
+
}
|
|
1472
|
+
if (created || modified) {
|
|
1473
|
+
writeFileSync(configPath, JSON.stringify(parsed, null, 2) + "\n");
|
|
1474
|
+
if (created) R.success("Created wrangler.jsonc with compatibility_date and nodejs_als");
|
|
1475
|
+
else R.success("Updated wrangler.jsonc with compatibility_date and nodejs_als");
|
|
1476
|
+
} else R.success("wrangler.jsonc looks good");
|
|
1477
|
+
}
|
|
1478
|
+
function writeVoidConfig(root, config) {
|
|
1479
|
+
writeFileSync(join(root, VOID_CONFIG_FILE), JSON.stringify(config, null, 2) + "\n");
|
|
1480
|
+
}
|
|
1481
|
+
function ensureVoidWorkerCompatibilityDate(root) {
|
|
1482
|
+
const config = readConfig(root);
|
|
1483
|
+
if (typeof config.target === "string" && config.target !== "cloudflare") return;
|
|
1484
|
+
const worker = asRecord(config.worker) ?? {};
|
|
1485
|
+
if (typeof worker.compatibility_date === "string" && worker.compatibility_date.length > 0) return;
|
|
1486
|
+
const compatibilityDate = readProjectWranglerCompatDate(root) ?? getTodayCompatDate();
|
|
1487
|
+
config.worker = {
|
|
1488
|
+
...worker,
|
|
1489
|
+
compatibility_date: compatibilityDate
|
|
1490
|
+
};
|
|
1491
|
+
writeVoidConfig(root, config);
|
|
1492
|
+
R.success("Pinned worker compatibility_date in void.json");
|
|
1493
|
+
}
|
|
1494
|
+
function clearConfiguredDatabase(root, messages) {
|
|
1495
|
+
const config = readConfig(root);
|
|
1496
|
+
const configPath = join(root, VOID_CONFIG_FILE);
|
|
1497
|
+
if (config.database !== "pg") {
|
|
1498
|
+
R.info(messages.unchanged);
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
delete config.database;
|
|
1502
|
+
if (Object.keys(config).length === 0) {
|
|
1503
|
+
unlinkSync(configPath);
|
|
1504
|
+
R.success(messages.updated);
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
writeVoidConfig(root, config);
|
|
1508
|
+
R.success(messages.updated);
|
|
1509
|
+
}
|
|
1510
|
+
function applyDatabaseDialect(root, dialect) {
|
|
1511
|
+
const config = readConfig(root);
|
|
1512
|
+
if (dialect === "postgresql") {
|
|
1513
|
+
if (config.database === "pg") {
|
|
1514
|
+
R.info("PostgreSQL already configured in void.json");
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
config.database = "pg";
|
|
1518
|
+
writeVoidConfig(root, config);
|
|
1519
|
+
R.success("Configured PostgreSQL in void.json");
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
clearConfiguredDatabase(root, {
|
|
1523
|
+
unchanged: "Using default D1 (SQLite) configuration",
|
|
1524
|
+
updated: "Updated void.json to use the default D1 (SQLite) configuration"
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
function detectDatabaseSetup(root) {
|
|
1528
|
+
if (getDatabaseDialect(readConfig(root)) === "postgresql") return "postgresql";
|
|
1529
|
+
if (hasDrizzleSchema(root) || existsSync(join(root, "db", "migrations")) || existsSync(join(root, "migrations"))) return "sqlite";
|
|
1530
|
+
return "none";
|
|
1531
|
+
}
|
|
1532
|
+
function applyDatabaseSetup(root, choice) {
|
|
1533
|
+
if (choice === "none") {
|
|
1534
|
+
clearConfiguredDatabase(root, {
|
|
1535
|
+
unchanged: "No database configured yet",
|
|
1536
|
+
updated: "Removed database config from void.json"
|
|
1537
|
+
});
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
applyDatabaseDialect(root, choice);
|
|
1541
|
+
}
|
|
1542
|
+
async function promptStarterMode() {
|
|
1543
|
+
const choice = await Ee({
|
|
1544
|
+
message: "Choose a Pages starter:",
|
|
1545
|
+
options: [
|
|
1546
|
+
{
|
|
1547
|
+
value: "sqlite",
|
|
1548
|
+
label: "D1 (SQLite)",
|
|
1549
|
+
hint: "Recommended starter. Scaffolds a DB-backed page, schema, generated migration, seed file, and API route."
|
|
1550
|
+
},
|
|
1551
|
+
{
|
|
1552
|
+
value: "postgresql",
|
|
1553
|
+
label: "PostgreSQL",
|
|
1554
|
+
hint: "Scaffolds the same starter with PostgreSQL, a generated migration, a seed file, and writes \"database\": \"pg\" to void.json."
|
|
1555
|
+
},
|
|
1556
|
+
{
|
|
1557
|
+
value: "static",
|
|
1558
|
+
label: "Static Pages",
|
|
1559
|
+
hint: "No database or server starter files yet. Good for content sites; add Void features later."
|
|
1560
|
+
}
|
|
1561
|
+
]
|
|
1562
|
+
});
|
|
1563
|
+
if (q(choice)) {
|
|
1564
|
+
me("Setup cancelled.");
|
|
1565
|
+
process.exit(0);
|
|
1566
|
+
}
|
|
1567
|
+
return choice;
|
|
1568
|
+
}
|
|
1569
|
+
async function promptScaffoldFramework() {
|
|
1570
|
+
const choice = await Ee({
|
|
1571
|
+
message: "Choose a Pages framework to scaffold:",
|
|
1572
|
+
options: [...SCAFFOLD_FRAMEWORK_OPTIONS]
|
|
1573
|
+
});
|
|
1574
|
+
if (q(choice)) {
|
|
1575
|
+
me("Setup cancelled.");
|
|
1576
|
+
process.exit(0);
|
|
1577
|
+
}
|
|
1578
|
+
return choice;
|
|
1579
|
+
}
|
|
1580
|
+
async function promptDatabaseSetup(root) {
|
|
1581
|
+
const currentSetup = detectDatabaseSetup(root);
|
|
1582
|
+
const choice = await Ee({
|
|
1583
|
+
message: "Choose a database setup for this project:",
|
|
1584
|
+
initialValue: currentSetup === "none" ? "sqlite" : currentSetup,
|
|
1585
|
+
options: [
|
|
1586
|
+
{
|
|
1587
|
+
value: "sqlite",
|
|
1588
|
+
label: currentSetup === "sqlite" ? "D1 (SQLite) [current]" : "D1 (SQLite)",
|
|
1589
|
+
hint: "Zero-config and fully managed by Void; best for prototyping and read-heavy apps."
|
|
1590
|
+
},
|
|
1591
|
+
{
|
|
1592
|
+
value: "postgresql",
|
|
1593
|
+
label: currentSetup === "postgresql" ? "PostgreSQL [current]" : "PostgreSQL",
|
|
1594
|
+
hint: "Bring your own Postgres via Hyperdrive; best for write-heavy apps, complex queries, or existing infra."
|
|
1595
|
+
},
|
|
1596
|
+
{
|
|
1597
|
+
value: "none",
|
|
1598
|
+
label: "No Database Yet",
|
|
1599
|
+
hint: "Skip database setup for now. You can add D1 or PostgreSQL later as your app grows."
|
|
1600
|
+
}
|
|
1601
|
+
]
|
|
1602
|
+
});
|
|
1603
|
+
if (q(choice)) {
|
|
1604
|
+
me("Setup cancelled.");
|
|
1605
|
+
process.exit(0);
|
|
1606
|
+
}
|
|
1607
|
+
return choice;
|
|
1608
|
+
}
|
|
1609
|
+
/** Get the dev command for a detected framework. */
|
|
1610
|
+
function getDevCommand(root, framework) {
|
|
1611
|
+
if (!framework) return formatProjectCommand(root, "vite dev");
|
|
1612
|
+
switch (framework.name) {
|
|
1613
|
+
case "nuxt": return "nuxt dev";
|
|
1614
|
+
case "astro": return "astro dev";
|
|
1615
|
+
default: return formatProjectCommand(root, "vite dev");
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
function getNodeVersionLine(root) {
|
|
1619
|
+
if (existsSync(join(root, ".node-version"))) return "node-version-file: .node-version";
|
|
1620
|
+
return "node-version: lts/*";
|
|
1621
|
+
}
|
|
1622
|
+
function getWorkflowYaml(root, pm) {
|
|
1623
|
+
const nodeVersion = getNodeVersionLine(root);
|
|
1624
|
+
switch (pm) {
|
|
1625
|
+
case "pnpm": return `name: Deploy
|
|
1626
|
+
on:
|
|
1627
|
+
push:
|
|
1628
|
+
branches: [main]
|
|
1629
|
+
|
|
1630
|
+
jobs:
|
|
1631
|
+
deploy:
|
|
1632
|
+
runs-on: ubuntu-latest
|
|
1633
|
+
steps:
|
|
1634
|
+
- uses: actions/checkout@v6
|
|
1635
|
+
- uses: pnpm/action-setup@v5
|
|
1636
|
+
- uses: actions/setup-node@v6
|
|
1637
|
+
with:
|
|
1638
|
+
${nodeVersion}
|
|
1639
|
+
cache: pnpm
|
|
1640
|
+
- run: pnpm install
|
|
1641
|
+
- run: pnpm void deploy
|
|
1642
|
+
env:
|
|
1643
|
+
VOID_TOKEN: \${{ secrets.VOID_TOKEN }}
|
|
1644
|
+
`;
|
|
1645
|
+
case "yarn": return `name: Deploy
|
|
1646
|
+
on:
|
|
1647
|
+
push:
|
|
1648
|
+
branches: [main]
|
|
1649
|
+
|
|
1650
|
+
jobs:
|
|
1651
|
+
deploy:
|
|
1652
|
+
runs-on: ubuntu-latest
|
|
1653
|
+
steps:
|
|
1654
|
+
- uses: actions/checkout@v6
|
|
1655
|
+
- uses: actions/setup-node@v6
|
|
1656
|
+
with:
|
|
1657
|
+
${nodeVersion}
|
|
1658
|
+
cache: yarn
|
|
1659
|
+
- run: yarn install --immutable
|
|
1660
|
+
- run: yarn void deploy
|
|
1661
|
+
env:
|
|
1662
|
+
VOID_TOKEN: \${{ secrets.VOID_TOKEN }}
|
|
1663
|
+
`;
|
|
1664
|
+
case "bun": return `name: Deploy
|
|
1665
|
+
on:
|
|
1666
|
+
push:
|
|
1667
|
+
branches: [main]
|
|
1668
|
+
|
|
1669
|
+
jobs:
|
|
1670
|
+
deploy:
|
|
1671
|
+
runs-on: ubuntu-latest
|
|
1672
|
+
steps:
|
|
1673
|
+
- uses: actions/checkout@v6
|
|
1674
|
+
- uses: oven-sh/setup-bun@v2
|
|
1675
|
+
- run: bun install
|
|
1676
|
+
- run: bunx void deploy
|
|
1677
|
+
env:
|
|
1678
|
+
VOID_TOKEN: \${{ secrets.VOID_TOKEN }}
|
|
1679
|
+
`;
|
|
1680
|
+
default: return `name: Deploy
|
|
1681
|
+
on:
|
|
1682
|
+
push:
|
|
1683
|
+
branches: [main]
|
|
1684
|
+
|
|
1685
|
+
jobs:
|
|
1686
|
+
deploy:
|
|
1687
|
+
runs-on: ubuntu-latest
|
|
1688
|
+
steps:
|
|
1689
|
+
- uses: actions/checkout@v6
|
|
1690
|
+
- uses: actions/setup-node@v6
|
|
1691
|
+
with:
|
|
1692
|
+
${nodeVersion}
|
|
1693
|
+
cache: npm
|
|
1694
|
+
- run: npm ci
|
|
1695
|
+
- run: npx void deploy
|
|
1696
|
+
env:
|
|
1697
|
+
VOID_TOKEN: \${{ secrets.VOID_TOKEN }}
|
|
1698
|
+
`;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
function setupGithubActions(root, state) {
|
|
1702
|
+
const workflowPath = join(root, ".github", "workflows", "deploy.yml");
|
|
1703
|
+
if (existsSync(workflowPath)) {
|
|
1704
|
+
R.warn("deploy.yml already exists, skipping");
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
mkdirSync(join(root, ".github", "workflows"), { recursive: true });
|
|
1708
|
+
writeFileSync(workflowPath, getWorkflowYaml(root, state.packageManager));
|
|
1709
|
+
R.success("Created .github/workflows/deploy.yml");
|
|
1710
|
+
Se("After init completes, run `npx void auth token` to copy a deploy token.\nThen add it in GitHub under Settings → Secrets and variables → Actions as `VOID_TOKEN`.", "GitHub Actions deploy token");
|
|
1711
|
+
}
|
|
1712
|
+
function writeFileIfMissing(root, filePath, content) {
|
|
1713
|
+
const fullPath = join(root, filePath);
|
|
1714
|
+
if (existsSync(fullPath)) return;
|
|
1715
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
1716
|
+
writeFileSync(fullPath, content);
|
|
1717
|
+
R.success(`Created ${filePath}`);
|
|
1718
|
+
}
|
|
1719
|
+
function ensureGitignore(root) {
|
|
1720
|
+
const gitignorePath = join(root, ".gitignore");
|
|
1721
|
+
if (!existsSync(gitignorePath)) {
|
|
1722
|
+
writeFileSync(gitignorePath, SCAFFOLD_GITIGNORE_LINES.join("\n") + "\n");
|
|
1723
|
+
R.success("Created .gitignore");
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
1727
|
+
const existing = new Set(content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
|
|
1728
|
+
const missing = SCAFFOLD_GITIGNORE_LINES.filter((line) => !existing.has(line));
|
|
1729
|
+
if (missing.length === 0) return;
|
|
1730
|
+
writeFileSync(gitignorePath, content + (content.endsWith("\n") ? "" : "\n") + missing.join("\n") + "\n");
|
|
1731
|
+
R.success("Updated .gitignore with Void defaults");
|
|
1732
|
+
}
|
|
1733
|
+
function ensurePnpmBuiltDependencies(pkg) {
|
|
1734
|
+
const pnpm = asRecord(pkg.pnpm) ?? {};
|
|
1735
|
+
pkg.pnpm = pnpm;
|
|
1736
|
+
const onlyBuiltDependencies = Array.isArray(pnpm.onlyBuiltDependencies) ? pnpm.onlyBuiltDependencies.filter((value) => typeof value === "string") : [];
|
|
1737
|
+
for (const dependency of PNPM_ONLY_BUILT_DEPENDENCIES) if (!onlyBuiltDependencies.includes(dependency)) onlyBuiltDependencies.push(dependency);
|
|
1738
|
+
pnpm.onlyBuiltDependencies = onlyBuiltDependencies;
|
|
1739
|
+
}
|
|
1740
|
+
function installProjectDependencies(root, packageManager) {
|
|
1741
|
+
R.step(`Installing dependencies with ${packageManager}`);
|
|
1742
|
+
const result = spawnSync(packageManager, ["install"], {
|
|
1743
|
+
cwd: root,
|
|
1744
|
+
stdio: "inherit",
|
|
1745
|
+
shell: process.platform === "win32"
|
|
1746
|
+
});
|
|
1747
|
+
if (result.error) throw result.error;
|
|
1748
|
+
if (result.status !== 0) throw new Error(`${packageManager} install failed with exit code ${result.status ?? "unknown"}`);
|
|
1749
|
+
R.success(`Installed dependencies with ${packageManager}`);
|
|
1750
|
+
}
|
|
1751
|
+
function generateStarterMigration(root, starterMode) {
|
|
1752
|
+
const dialect = starterMode === "postgresql" ? "postgresql" : "sqlite";
|
|
1753
|
+
const configPath = writeDrizzleConfig(root, dialect === "postgresql" ? "postgresql://dummy" : ":memory:", dialect);
|
|
1754
|
+
const migrationsDir = join(root, "db", "migrations");
|
|
1755
|
+
const metaDir = join(migrationsDir, "meta");
|
|
1756
|
+
const journalPath = join(metaDir, "_journal.json");
|
|
1757
|
+
const migrationsDirExistedBefore = existsSync(migrationsDir);
|
|
1758
|
+
const metaDirExistedBefore = existsSync(metaDir);
|
|
1759
|
+
const originalJournalBytes = existsSync(journalPath) ? readFileSync(journalPath) : null;
|
|
1760
|
+
const snapshotsBefore = metaDirExistedBefore ? new Set(readdirSync(metaDir).filter((f) => f.endsWith("_snapshot.json"))) : /* @__PURE__ */ new Set();
|
|
1761
|
+
const sqlFilesBefore = migrationsDirExistedBefore ? new Set(readdirSync(migrationsDir).filter((f) => f.endsWith(".sql"))) : /* @__PURE__ */ new Set();
|
|
1762
|
+
R.step("Generating initial Drizzle migration");
|
|
1763
|
+
const result = spawnSync(process.execPath, [
|
|
1764
|
+
drizzleKitBin,
|
|
1765
|
+
"generate",
|
|
1766
|
+
"--config",
|
|
1767
|
+
configPath
|
|
1768
|
+
], {
|
|
1769
|
+
cwd: root,
|
|
1770
|
+
stdio: "pipe",
|
|
1771
|
+
encoding: "utf-8",
|
|
1772
|
+
shell: process.platform === "win32",
|
|
1773
|
+
env: buildDrizzleKitSpawnEnv(root)
|
|
1774
|
+
});
|
|
1775
|
+
if (Boolean(result.error) || result.status !== 0) {
|
|
1776
|
+
if (originalJournalBytes !== null) try {
|
|
1777
|
+
writeFileSync(journalPath, originalJournalBytes);
|
|
1778
|
+
} catch {}
|
|
1779
|
+
else if (!metaDirExistedBefore && existsSync(metaDir)) try {
|
|
1780
|
+
for (const f of readdirSync(metaDir)) try {
|
|
1781
|
+
unlinkSync(join(metaDir, f));
|
|
1782
|
+
} catch {}
|
|
1783
|
+
try {
|
|
1784
|
+
rmdirSync(metaDir);
|
|
1785
|
+
} catch {}
|
|
1786
|
+
} catch {}
|
|
1787
|
+
if (metaDirExistedBefore && existsSync(metaDir)) {
|
|
1788
|
+
for (const f of readdirSync(metaDir)) if (f.endsWith("_snapshot.json") && !snapshotsBefore.has(f)) try {
|
|
1789
|
+
unlinkSync(join(metaDir, f));
|
|
1790
|
+
} catch {}
|
|
1791
|
+
}
|
|
1792
|
+
if (!migrationsDirExistedBefore && existsSync(migrationsDir)) try {
|
|
1793
|
+
for (const f of readdirSync(migrationsDir)) if (f.endsWith(".sql")) try {
|
|
1794
|
+
unlinkSync(join(migrationsDir, f));
|
|
1795
|
+
} catch {}
|
|
1796
|
+
} catch {}
|
|
1797
|
+
else if (migrationsDirExistedBefore && existsSync(migrationsDir)) {
|
|
1798
|
+
for (const f of readdirSync(migrationsDir)) if (f.endsWith(".sql") && !sqlFilesBefore.has(f)) try {
|
|
1799
|
+
unlinkSync(join(migrationsDir, f));
|
|
1800
|
+
} catch {}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (result.error) throw result.error;
|
|
1804
|
+
if (result.status !== 0) {
|
|
1805
|
+
const detail = [result.stderr, result.stdout].filter(Boolean).join("\n").trim();
|
|
1806
|
+
throw new Error(["Failed to generate the starter database migration.", ...detail ? [detail] : []].join("\n\n"));
|
|
1807
|
+
}
|
|
1808
|
+
R.success("Generated initial Drizzle migration");
|
|
1809
|
+
}
|
|
1810
|
+
function scaffoldPagesApp(root, packageManager, framework, starterMode) {
|
|
1811
|
+
const packageJsonPath = join(root, "package.json");
|
|
1812
|
+
const template = createPagesStarterTemplate({
|
|
1813
|
+
framework,
|
|
1814
|
+
starterMode,
|
|
1815
|
+
voidPackageSpec: resolveScaffoldDependencySpec(root, "void", getOwnVersion()),
|
|
1816
|
+
adapterPackageSpec: resolveScaffoldDependencySpec(root, SCAFFOLD_FRAMEWORKS[framework].adapterPackage, getOwnVersion())
|
|
1817
|
+
});
|
|
1818
|
+
let pkg = {};
|
|
1819
|
+
if (existsSync(packageJsonPath)) try {
|
|
1820
|
+
pkg = asRecord(JSON.parse(readFileSync(packageJsonPath, "utf-8"))) ?? {};
|
|
1821
|
+
} catch {
|
|
1822
|
+
throw new Error("Could not parse package.json in empty project");
|
|
1823
|
+
}
|
|
1824
|
+
const fallbackName = basename(root).toLowerCase().replace(/[^a-z0-9-]/g, "-") || "my-app";
|
|
1825
|
+
if (!pkg.name || typeof pkg.name !== "string") pkg.name = fallbackName;
|
|
1826
|
+
if (pkg.private === void 0) pkg.private = true;
|
|
1827
|
+
if (!pkg.type) pkg.type = "module";
|
|
1828
|
+
const scripts = asRecord(pkg.scripts) ?? {};
|
|
1829
|
+
pkg.scripts = scripts;
|
|
1830
|
+
for (const [name, command] of Object.entries(template.scripts)) if (!scripts[name]) scripts[name] = command;
|
|
1831
|
+
const dependencies = asRecord(pkg.dependencies) ?? {};
|
|
1832
|
+
const devDependencies = asRecord(pkg.devDependencies) ?? {};
|
|
1833
|
+
pkg.dependencies = dependencies;
|
|
1834
|
+
pkg.devDependencies = devDependencies;
|
|
1835
|
+
let dependencyAdded = false;
|
|
1836
|
+
const ensureDependency = (group, name, fallbackVersion) => {
|
|
1837
|
+
const otherGroup = group === "dependencies" ? "devDependencies" : "dependencies";
|
|
1838
|
+
const targetGroup = group === "dependencies" ? dependencies : devDependencies;
|
|
1839
|
+
const sourceGroup = otherGroup === "dependencies" ? dependencies : devDependencies;
|
|
1840
|
+
const existing = targetGroup[name] ?? sourceGroup[name];
|
|
1841
|
+
if (!targetGroup[name]) {
|
|
1842
|
+
targetGroup[name] = existing ?? fallbackVersion;
|
|
1843
|
+
dependencyAdded = true;
|
|
1844
|
+
}
|
|
1845
|
+
if (sourceGroup[name]) {
|
|
1846
|
+
delete sourceGroup[name];
|
|
1847
|
+
dependencyAdded = true;
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
for (const [name, version] of Object.entries(template.dependencies)) ensureDependency("dependencies", name, version);
|
|
1851
|
+
for (const [name, version] of Object.entries(template.devDependencies)) ensureDependency("devDependencies", name, version);
|
|
1852
|
+
if (isPnpmProject(root, pkg)) ensurePnpmBuiltDependencies(pkg);
|
|
1853
|
+
if (packageManager === "pnpm" && (!pkg.packageManager || typeof pkg.packageManager !== "string")) pkg.packageManager = "pnpm@10.33.0";
|
|
1854
|
+
writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
1855
|
+
R.success("Updated package.json with starter scripts and dependencies");
|
|
1856
|
+
for (const [filePath, content] of Object.entries(template.files)) writeFileIfMissing(root, filePath, content);
|
|
1857
|
+
ensureGitignore(root);
|
|
1858
|
+
if (starterMode !== "static") applyDatabaseDialect(root, starterMode);
|
|
1859
|
+
if (starterMode === "postgresql") Se(`Add DATABASE_URL to .env.local before running \`${getDevCommand(root)}\`.\nVoid uses that direct connection for local PostgreSQL development.`, "PostgreSQL local dev");
|
|
1860
|
+
return dependencyAdded;
|
|
1861
|
+
}
|
|
1862
|
+
function hasDeclaredDependency(pkg, name) {
|
|
1863
|
+
return DEPENDENCY_FIELDS.some((field) => {
|
|
1864
|
+
return typeof asRecord(pkg[field])?.[name] === "string";
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
function ensureExistingProjectPackage(root, options = {}) {
|
|
1868
|
+
const packageJsonPath = join(root, "package.json");
|
|
1869
|
+
const voidSpec = resolveScaffoldDependencySpec(root, "void", getOwnVersion());
|
|
1870
|
+
const plainViteSetup = options.plainViteSetup ?? true;
|
|
1871
|
+
let pkg = {};
|
|
1872
|
+
let existedBefore = false;
|
|
1873
|
+
if (existsSync(packageJsonPath)) {
|
|
1874
|
+
existedBefore = true;
|
|
1875
|
+
try {
|
|
1876
|
+
pkg = asRecord(JSON.parse(readFileSync(packageJsonPath, "utf-8"))) ?? {};
|
|
1877
|
+
} catch {
|
|
1878
|
+
throw new Error("Could not parse package.json while configuring Void");
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
const fallbackName = basename(root).toLowerCase().replace(/[^a-z0-9-]/g, "-") || "my-app";
|
|
1882
|
+
let changed = !existedBefore;
|
|
1883
|
+
let dependencyAdded = false;
|
|
1884
|
+
if (!pkg.name || typeof pkg.name !== "string") {
|
|
1885
|
+
pkg.name = fallbackName;
|
|
1886
|
+
changed = true;
|
|
1887
|
+
}
|
|
1888
|
+
if (!existedBefore && pkg.private === void 0) {
|
|
1889
|
+
pkg.private = true;
|
|
1890
|
+
changed = true;
|
|
1891
|
+
}
|
|
1892
|
+
if (!existedBefore && !pkg.type) {
|
|
1893
|
+
pkg.type = "module";
|
|
1894
|
+
changed = true;
|
|
1895
|
+
}
|
|
1896
|
+
if (plainViteSetup) {
|
|
1897
|
+
const scripts = asRecord(pkg.scripts) ?? {};
|
|
1898
|
+
pkg.scripts = scripts;
|
|
1899
|
+
if (typeof scripts.dev !== "string") {
|
|
1900
|
+
scripts.dev = "vite";
|
|
1901
|
+
changed = true;
|
|
1902
|
+
}
|
|
1903
|
+
if (typeof scripts.build !== "string") {
|
|
1904
|
+
scripts.build = "vite build";
|
|
1905
|
+
changed = true;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
const devDependencies = asRecord(pkg.devDependencies) ?? {};
|
|
1909
|
+
pkg.devDependencies = devDependencies;
|
|
1910
|
+
if (!hasDeclaredDependency(pkg, "void")) {
|
|
1911
|
+
devDependencies.void = voidSpec;
|
|
1912
|
+
changed = true;
|
|
1913
|
+
dependencyAdded = true;
|
|
1914
|
+
}
|
|
1915
|
+
if (plainViteSetup && !hasDeclaredDependency(pkg, "vite")) {
|
|
1916
|
+
devDependencies.vite = SCAFFOLD_BASE_DEV_DEPENDENCIES.vite;
|
|
1917
|
+
changed = true;
|
|
1918
|
+
dependencyAdded = true;
|
|
1919
|
+
}
|
|
1920
|
+
if (isPnpmProject(root, pkg)) {
|
|
1921
|
+
const before = JSON.stringify(asRecord(pkg.pnpm)?.onlyBuiltDependencies ?? null);
|
|
1922
|
+
ensurePnpmBuiltDependencies(pkg);
|
|
1923
|
+
if (before !== JSON.stringify(asRecord(pkg.pnpm)?.onlyBuiltDependencies ?? null)) changed = true;
|
|
1924
|
+
}
|
|
1925
|
+
if (changed) {
|
|
1926
|
+
writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
1927
|
+
R.success(existedBefore ? plainViteSetup ? "Updated package.json with Void dependencies and Vite scripts" : "Updated package.json with Void dependency" : plainViteSetup ? "Created package.json with Void dependencies and Vite scripts" : "Created package.json with Void dependency");
|
|
1928
|
+
}
|
|
1929
|
+
return {
|
|
1930
|
+
dependencyAdded,
|
|
1931
|
+
packageJsonMutated: changed
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
function insertVoidImport(code) {
|
|
1935
|
+
if (/import\s*\{[^}]*\bvoidPlugin\b[^}]*\}\s*from\s*["']void["']/.test(code)) return code;
|
|
1936
|
+
const importLine = "import { voidPlugin } from \"void\";\n";
|
|
1937
|
+
const lines = code.split(/(?<=\n)/);
|
|
1938
|
+
let insertAt = 0;
|
|
1939
|
+
for (let i = 0; i < lines.length; i++) if (/^\s*import\b/.test(lines[i])) insertAt = i + 1;
|
|
1940
|
+
lines.splice(insertAt, 0, importLine);
|
|
1941
|
+
return lines.join("");
|
|
1942
|
+
}
|
|
1943
|
+
function isIdentifierStart(char) {
|
|
1944
|
+
return char !== void 0 && /[$A-Za-z_]/.test(char);
|
|
1945
|
+
}
|
|
1946
|
+
function isIdentifierPart(char) {
|
|
1947
|
+
return char !== void 0 && /[$0-9A-Za-z_]/.test(char);
|
|
1948
|
+
}
|
|
1949
|
+
function isKeywordAt(code, index, keyword) {
|
|
1950
|
+
return code.startsWith(keyword, index) && !isIdentifierPart(code[index - 1]) && !isIdentifierPart(code[index + keyword.length]);
|
|
1951
|
+
}
|
|
1952
|
+
function skipStringLiteral(code, index) {
|
|
1953
|
+
const quote = code[index];
|
|
1954
|
+
let i = index + 1;
|
|
1955
|
+
while (i < code.length) {
|
|
1956
|
+
const char = code[i];
|
|
1957
|
+
if (char === "\\") {
|
|
1958
|
+
i += 2;
|
|
1959
|
+
continue;
|
|
1960
|
+
}
|
|
1961
|
+
if (char === quote) return i + 1;
|
|
1962
|
+
i++;
|
|
1963
|
+
}
|
|
1964
|
+
return code.length;
|
|
1965
|
+
}
|
|
1966
|
+
function skipTemplateLiteral(code, index) {
|
|
1967
|
+
let i = index + 1;
|
|
1968
|
+
while (i < code.length) {
|
|
1969
|
+
const char = code[i];
|
|
1970
|
+
if (char === "\\") {
|
|
1971
|
+
i += 2;
|
|
1972
|
+
continue;
|
|
1973
|
+
}
|
|
1974
|
+
if (char === "`") return i + 1;
|
|
1975
|
+
i++;
|
|
1976
|
+
}
|
|
1977
|
+
return code.length;
|
|
1978
|
+
}
|
|
1979
|
+
function skipIgnored(code, index) {
|
|
1980
|
+
const char = code[index];
|
|
1981
|
+
if (char === "\"" || char === "'") return skipStringLiteral(code, index);
|
|
1982
|
+
if (char === "`") return skipTemplateLiteral(code, index);
|
|
1983
|
+
if (char === "/" && code[index + 1] === "/") {
|
|
1984
|
+
const newline = code.indexOf("\n", index + 2);
|
|
1985
|
+
return newline === -1 ? code.length : newline + 1;
|
|
1986
|
+
}
|
|
1987
|
+
if (char === "/" && code[index + 1] === "*") {
|
|
1988
|
+
const end = code.indexOf("*/", index + 2);
|
|
1989
|
+
return end === -1 ? code.length : end + 2;
|
|
1990
|
+
}
|
|
1991
|
+
return index;
|
|
1992
|
+
}
|
|
1993
|
+
function skipTrivia(code, index) {
|
|
1994
|
+
let i = index;
|
|
1995
|
+
while (i < code.length) {
|
|
1996
|
+
if (/\s/.test(code[i])) {
|
|
1997
|
+
i++;
|
|
1998
|
+
continue;
|
|
1999
|
+
}
|
|
2000
|
+
const next = skipIgnored(code, i);
|
|
2001
|
+
if (next === i) return i;
|
|
2002
|
+
i = next;
|
|
2003
|
+
}
|
|
2004
|
+
return i;
|
|
2005
|
+
}
|
|
2006
|
+
function skipIdentifier(code, index) {
|
|
2007
|
+
let i = index;
|
|
2008
|
+
while (isIdentifierPart(code[i])) i++;
|
|
2009
|
+
return i;
|
|
2010
|
+
}
|
|
2011
|
+
function skipBalanced(code, start, open, close) {
|
|
2012
|
+
let depth = 1;
|
|
2013
|
+
let i = start + 1;
|
|
2014
|
+
while (i < code.length) {
|
|
2015
|
+
const ignored = skipIgnored(code, i);
|
|
2016
|
+
if (ignored !== i) {
|
|
2017
|
+
i = ignored;
|
|
2018
|
+
continue;
|
|
2019
|
+
}
|
|
2020
|
+
const char = code[i];
|
|
2021
|
+
if (char === open) depth++;
|
|
2022
|
+
else if (char === close) {
|
|
2023
|
+
depth--;
|
|
2024
|
+
if (depth === 0) return i;
|
|
2025
|
+
}
|
|
2026
|
+
i++;
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
function skipTypeArguments(code, index) {
|
|
2030
|
+
const start = skipTrivia(code, index);
|
|
2031
|
+
if (code[start] !== "<") return start;
|
|
2032
|
+
const end = skipBalanced(code, start, "<", ">");
|
|
2033
|
+
return end === void 0 ? start : skipTrivia(code, end + 1);
|
|
2034
|
+
}
|
|
2035
|
+
function findTopLevelConfigObject(code) {
|
|
2036
|
+
let braceDepth = 0;
|
|
2037
|
+
let bracketDepth = 0;
|
|
2038
|
+
let parenDepth = 0;
|
|
2039
|
+
let i = 0;
|
|
2040
|
+
while (i < code.length) {
|
|
2041
|
+
const ignored = skipIgnored(code, i);
|
|
2042
|
+
if (ignored !== i) {
|
|
2043
|
+
i = ignored;
|
|
2044
|
+
continue;
|
|
2045
|
+
}
|
|
2046
|
+
if (braceDepth === 0 && bracketDepth === 0 && parenDepth === 0 && isKeywordAt(code, i, "export")) {
|
|
2047
|
+
const defaultIndex = skipTrivia(code, i + 6);
|
|
2048
|
+
if (!isKeywordAt(code, defaultIndex, "default")) {
|
|
2049
|
+
i++;
|
|
2050
|
+
continue;
|
|
2051
|
+
}
|
|
2052
|
+
let expressionStart = skipTrivia(code, defaultIndex + 7);
|
|
2053
|
+
if (code[expressionStart] === "{") {
|
|
2054
|
+
const end = skipBalanced(code, expressionStart, "{", "}");
|
|
2055
|
+
return end === void 0 ? void 0 : {
|
|
2056
|
+
start: expressionStart,
|
|
2057
|
+
end
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
if (!isKeywordAt(code, expressionStart, "defineConfig")) return;
|
|
2061
|
+
expressionStart = skipTypeArguments(code, expressionStart + 12);
|
|
2062
|
+
if (code[expressionStart] !== "(") return;
|
|
2063
|
+
const firstArgumentStart = skipTrivia(code, expressionStart + 1);
|
|
2064
|
+
if (code[firstArgumentStart] !== "{") return;
|
|
2065
|
+
const end = skipBalanced(code, firstArgumentStart, "{", "}");
|
|
2066
|
+
return end === void 0 ? void 0 : {
|
|
2067
|
+
start: firstArgumentStart,
|
|
2068
|
+
end
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
const char = code[i];
|
|
2072
|
+
if (char === "{") braceDepth++;
|
|
2073
|
+
else if (char === "}") braceDepth = Math.max(0, braceDepth - 1);
|
|
2074
|
+
else if (char === "[") bracketDepth++;
|
|
2075
|
+
else if (char === "]") bracketDepth = Math.max(0, bracketDepth - 1);
|
|
2076
|
+
else if (char === "(") parenDepth++;
|
|
2077
|
+
else if (char === ")") parenDepth = Math.max(0, parenDepth - 1);
|
|
2078
|
+
i++;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
function readPropertyName(code, index) {
|
|
2082
|
+
const char = code[index];
|
|
2083
|
+
if (isIdentifierStart(char)) {
|
|
2084
|
+
const end = skipIdentifier(code, index);
|
|
2085
|
+
return {
|
|
2086
|
+
name: code.slice(index, end),
|
|
2087
|
+
end
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
if (char === "\"" || char === "'") {
|
|
2091
|
+
const end = skipStringLiteral(code, index);
|
|
2092
|
+
return {
|
|
2093
|
+
name: code.slice(index + 1, end - 1),
|
|
2094
|
+
end
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
function skipPropertyValue(code, index, objectEnd) {
|
|
2099
|
+
let braceDepth = 0;
|
|
2100
|
+
let bracketDepth = 0;
|
|
2101
|
+
let parenDepth = 0;
|
|
2102
|
+
let i = index;
|
|
2103
|
+
while (i < objectEnd) {
|
|
2104
|
+
const ignored = skipIgnored(code, i);
|
|
2105
|
+
if (ignored !== i) {
|
|
2106
|
+
i = ignored;
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
const char = code[i];
|
|
2110
|
+
if (braceDepth === 0 && bracketDepth === 0 && parenDepth === 0 && char === ",") return i + 1;
|
|
2111
|
+
if (char === "{") braceDepth++;
|
|
2112
|
+
else if (char === "}") {
|
|
2113
|
+
if (braceDepth === 0) return i;
|
|
2114
|
+
braceDepth--;
|
|
2115
|
+
} else if (char === "[") bracketDepth++;
|
|
2116
|
+
else if (char === "]") {
|
|
2117
|
+
if (bracketDepth === 0) return i;
|
|
2118
|
+
bracketDepth--;
|
|
2119
|
+
} else if (char === "(") parenDepth++;
|
|
2120
|
+
else if (char === ")") {
|
|
2121
|
+
if (parenDepth === 0) return i;
|
|
2122
|
+
parenDepth--;
|
|
2123
|
+
}
|
|
2124
|
+
i++;
|
|
2125
|
+
}
|
|
2126
|
+
return i;
|
|
2127
|
+
}
|
|
2128
|
+
function findTopLevelPluginsProperty(code, object) {
|
|
2129
|
+
let i = object.start + 1;
|
|
2130
|
+
while (i < object.end) {
|
|
2131
|
+
i = skipTrivia(code, i);
|
|
2132
|
+
if (code[i] === ",") {
|
|
2133
|
+
i++;
|
|
2134
|
+
continue;
|
|
2135
|
+
}
|
|
2136
|
+
const propertyStart = i;
|
|
2137
|
+
const propertyName = readPropertyName(code, i);
|
|
2138
|
+
if (!propertyName) {
|
|
2139
|
+
i = skipPropertyValue(code, i, object.end);
|
|
2140
|
+
continue;
|
|
2141
|
+
}
|
|
2142
|
+
const colon = skipTrivia(code, propertyName.end);
|
|
2143
|
+
if (code[colon] !== ":") {
|
|
2144
|
+
i = skipPropertyValue(code, propertyName.end, object.end);
|
|
2145
|
+
continue;
|
|
2146
|
+
}
|
|
2147
|
+
const valueStart = skipTrivia(code, colon + 1);
|
|
2148
|
+
if (propertyName.name === "plugins") {
|
|
2149
|
+
if (code[valueStart] !== "[") return "unsupported";
|
|
2150
|
+
const arrayEnd = skipBalanced(code, valueStart, "[", "]");
|
|
2151
|
+
return arrayEnd === void 0 ? "unsupported" : {
|
|
2152
|
+
arrayStart: valueStart,
|
|
2153
|
+
arrayEnd,
|
|
2154
|
+
propertyStart
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
i = skipPropertyValue(code, valueStart, object.end);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
function getLineIndent(code, index) {
|
|
2161
|
+
const lineStart = code.lastIndexOf("\n", index) + 1;
|
|
2162
|
+
return code.slice(lineStart, index).match(/^\s*/)?.[0] ?? "";
|
|
2163
|
+
}
|
|
2164
|
+
function insertVoidPluginIntoTopLevelConfig(code) {
|
|
2165
|
+
const object = findTopLevelConfigObject(code);
|
|
2166
|
+
if (!object) return;
|
|
2167
|
+
const pluginsProperty = findTopLevelPluginsProperty(code, object);
|
|
2168
|
+
const ms = new MagicString(code);
|
|
2169
|
+
if (pluginsProperty === "unsupported") return;
|
|
2170
|
+
if (pluginsProperty) {
|
|
2171
|
+
const arrayContent = code.slice(pluginsProperty.arrayStart + 1, pluginsProperty.arrayEnd);
|
|
2172
|
+
if (/^\s*$/.test(arrayContent)) {
|
|
2173
|
+
ms.overwrite(pluginsProperty.arrayStart + 1, pluginsProperty.arrayEnd, "voidPlugin()");
|
|
2174
|
+
return ms.toString();
|
|
2175
|
+
}
|
|
2176
|
+
const propIndent = getLineIndent(code, pluginsProperty.propertyStart);
|
|
2177
|
+
const insertAt = pluginsProperty.arrayStart + 1;
|
|
2178
|
+
const next = code[insertAt];
|
|
2179
|
+
const insertion = next === "\n" || next === "\r" ? `\n${propIndent} voidPlugin(),` : "voidPlugin(), ";
|
|
2180
|
+
ms.appendLeft(insertAt, insertion);
|
|
2181
|
+
return ms.toString();
|
|
2182
|
+
}
|
|
2183
|
+
const objectContent = code.slice(object.start + 1, object.end);
|
|
2184
|
+
if (/^\s*$/.test(objectContent)) {
|
|
2185
|
+
ms.overwrite(object.start + 1, object.end, " plugins: [voidPlugin()] ");
|
|
2186
|
+
return ms.toString();
|
|
2187
|
+
}
|
|
2188
|
+
const baseIndent = getLineIndent(code, object.start);
|
|
2189
|
+
const next = code[object.start + 1];
|
|
2190
|
+
const insertion = next === "\n" || next === "\r" ? `\n${baseIndent} plugins: [voidPlugin()],` : " plugins: [voidPlugin()],";
|
|
2191
|
+
ms.appendLeft(object.start + 1, insertion);
|
|
2192
|
+
return ms.toString();
|
|
2193
|
+
}
|
|
2194
|
+
function patchViteConfigWithVoidPlugin(filePath, code) {
|
|
2195
|
+
if (/\bvoidPlugin\s*\(/.test(code)) return code;
|
|
2196
|
+
if (filePath.endsWith(".cjs") || filePath.endsWith(".cts")) return;
|
|
2197
|
+
const withPlugin = insertVoidPluginIntoTopLevelConfig(code);
|
|
2198
|
+
if (!withPlugin) return;
|
|
2199
|
+
return insertVoidImport(withPlugin);
|
|
2200
|
+
}
|
|
2201
|
+
function getBasicVoidViteConfig() {
|
|
2202
|
+
return `import { defineConfig } from "vite";
|
|
2203
|
+
import { voidPlugin } from "void";
|
|
2204
|
+
|
|
2205
|
+
export default defineConfig({
|
|
2206
|
+
plugins: [voidPlugin()],
|
|
2207
|
+
});
|
|
2208
|
+
`;
|
|
2209
|
+
}
|
|
2210
|
+
function noteManualViteConfigSetup(configPath) {
|
|
2211
|
+
Se(`Could not safely update ${configPath ?? "your framework Vite config"} automatically.\n\nAdd \`voidPlugin()\` to the top-level Vite plugins array:\n\nimport { voidPlugin } from "void";\n\nexport default defineConfig({\n plugins: [voidPlugin()],\n});`, "Manual Vite config step");
|
|
2212
|
+
}
|
|
2213
|
+
function noteManualFrameworkConfigSetup(framework) {
|
|
2214
|
+
if (framework.name === "astro") {
|
|
2215
|
+
Se("Detected Astro. Void init does not patch astro.config.* automatically.\n\nAdd `voidPlugin()` to the Astro Vite plugins option:\n\nimport { voidPlugin } from \"void\";\n\nexport default defineConfig({\n vite: { plugins: [voidPlugin()] },\n});", "Manual Astro config step");
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
if (framework.name === "nuxt") {
|
|
2219
|
+
Se("Detected Nuxt. Void init does not patch nuxt.config.* automatically.\n\nAdd `voidPlugin()` to the Nuxt Vite plugins option:\n\nimport { voidPlugin } from \"void\";\n\nexport default defineNuxtConfig({\n vite: { plugins: [voidPlugin()] },\n});", "Manual Nuxt config step");
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
noteManualViteConfigSetup();
|
|
2223
|
+
}
|
|
2224
|
+
function ensureExistingProjectViteConfig(root, framework) {
|
|
2225
|
+
if (framework?.class === "c") {
|
|
2226
|
+
noteManualFrameworkConfigSetup(framework);
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
const configPath = findViteConfig(root);
|
|
2230
|
+
if (!configPath) {
|
|
2231
|
+
writeFileIfMissing(root, "vite.config.ts", getBasicVoidViteConfig());
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
const fullPath = join(root, configPath);
|
|
2235
|
+
const original = readFileSync(fullPath, "utf-8");
|
|
2236
|
+
const updated = patchViteConfigWithVoidPlugin(configPath, original);
|
|
2237
|
+
if (updated === void 0) {
|
|
2238
|
+
noteManualViteConfigSetup(configPath);
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
if (updated === original) {
|
|
2242
|
+
R.info(`${configPath} already includes voidPlugin()`);
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
writeFileSync(fullPath, updated);
|
|
2246
|
+
R.success(`Added voidPlugin() to ${configPath}`);
|
|
2247
|
+
}
|
|
2248
|
+
function setupExistingProject(root, framework) {
|
|
2249
|
+
R.info("Existing project detected. Configuring Void...");
|
|
2250
|
+
const packageResult = ensureExistingProjectPackage(root, { plainViteSetup: framework?.class !== "c" });
|
|
2251
|
+
ensureExistingProjectViteConfig(root, framework);
|
|
2252
|
+
return packageResult;
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Scaffolds the minimal "SQL → types → handler → fetch" demo into an
|
|
2256
|
+
* existing project. The demo files import from `void/db` and
|
|
2257
|
+
* `void/schema-d1`, so `void` MUST be present in `package.json` before
|
|
2258
|
+
* the caller runs `installProjectDependencies` +
|
|
2259
|
+
* `generateStarterMigration`; otherwise `drizzle-kit generate` cannot
|
|
2260
|
+
* resolve the schema imports. This function ensures `void` is declared,
|
|
2261
|
+
* preserving whatever spec is already there or adding the current CLI
|
|
2262
|
+
* version used by greenfield scaffolds.
|
|
2263
|
+
* `scaffoldPagesApp` has the same contract via its template; we just
|
|
2264
|
+
* replicate the "ensure void dep" slice here for the demo path.
|
|
2265
|
+
*/
|
|
2266
|
+
function scaffoldDemo(root) {
|
|
2267
|
+
const devCommand = getDevCommand(root);
|
|
2268
|
+
const packageJsonMutated = ensureVoidDependency(root);
|
|
2269
|
+
mkdirSync(join(root, "db"), { recursive: true });
|
|
2270
|
+
writeFileSync(join(root, "db", "schema.ts"), `import { sql } from "void/db";
|
|
2271
|
+
import { integer, sqliteTable, text } from "void/schema-d1";
|
|
2272
|
+
|
|
2273
|
+
export const posts = sqliteTable("posts", {
|
|
2274
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
2275
|
+
title: text("title").notNull(),
|
|
2276
|
+
body: text("body").notNull(),
|
|
2277
|
+
createdAt: text("created_at").notNull().default(sql\`(datetime('now'))\`),
|
|
2278
|
+
});
|
|
2279
|
+
`);
|
|
2280
|
+
R.success("Created db/schema.ts");
|
|
2281
|
+
mkdirSync(join(root, "routes", "api"), { recursive: true });
|
|
2282
|
+
writeFileSync(join(root, "routes", "api", "posts.ts"), `import { defineHandler } from "void";
|
|
2283
|
+
import { db } from "void/db";
|
|
2284
|
+
import { posts } from "@schema";
|
|
2285
|
+
|
|
2286
|
+
// GET /api/posts — returns all posts, fully typed from migration schema
|
|
2287
|
+
export const GET = defineHandler(async () => {
|
|
2288
|
+
return db.select().from(posts);
|
|
2289
|
+
});
|
|
2290
|
+
`);
|
|
2291
|
+
R.success("Created routes/api/posts.ts");
|
|
2292
|
+
ensureProjectTsconfig(root, void 0, true, true);
|
|
2293
|
+
mkdirSync(join(root, "src"), { recursive: true });
|
|
2294
|
+
writeFileSync(join(root, "src", "demo.ts"), `import { fetch } from "void/client";
|
|
2295
|
+
|
|
2296
|
+
// Fully typed: response type is inferred from the GET handler's return type,
|
|
2297
|
+
// which is derived from the migration schema. No manual types needed.
|
|
2298
|
+
const posts = await fetch("/api/posts");
|
|
2299
|
+
|
|
2300
|
+
console.log(posts);
|
|
2301
|
+
// posts[0].title ← string (type-checked)
|
|
2302
|
+
// posts[0].body ← string (type-checked)
|
|
2303
|
+
// posts[0].nope ← TypeScript error
|
|
2304
|
+
`);
|
|
2305
|
+
R.success("Created src/demo.ts");
|
|
2306
|
+
Se(`SQL migration → DB types → Route handler → Route types → Typed fetch
|
|
2307
|
+
|
|
2308
|
+
Run \`${devCommand}\` and open src/demo.ts to see the types in action.\nTry changing the migration or handler — types update automatically.`, "Type Safety Demo");
|
|
2309
|
+
return { packageJsonMutated };
|
|
2310
|
+
}
|
|
2311
|
+
/**
|
|
2312
|
+
* Ensures `void` is declared in `package.json`, creating the file if it
|
|
2313
|
+
* does not exist. This is a narrow, scaffoldDemo-specific helper:
|
|
2314
|
+
* `scaffoldPagesApp` handles greenfield projects via
|
|
2315
|
+
* `createPagesStarterTemplate` (which knows about adapters,
|
|
2316
|
+
* devDependencies, scripts, etc.); `scaffoldDemo` only needs enough in
|
|
2317
|
+
* `package.json` for `drizzle-kit generate` to resolve `void/schema-d1`
|
|
2318
|
+
* via `node_modules/void` after install.
|
|
2319
|
+
*/
|
|
2320
|
+
function ensureVoidDependency(root) {
|
|
2321
|
+
const packageJsonPath = join(root, "package.json");
|
|
2322
|
+
const voidSpec = resolveScaffoldDependencySpec(root, "void", getOwnVersion());
|
|
2323
|
+
let pkg = {};
|
|
2324
|
+
let existedBefore = false;
|
|
2325
|
+
if (existsSync(packageJsonPath)) {
|
|
2326
|
+
existedBefore = true;
|
|
2327
|
+
try {
|
|
2328
|
+
pkg = asRecord(JSON.parse(readFileSync(packageJsonPath, "utf-8"))) ?? {};
|
|
2329
|
+
} catch {
|
|
2330
|
+
throw new Error("Could not parse package.json while scaffolding demo");
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
const fallbackName = basename(root).toLowerCase().replace(/[^a-z0-9-]/g, "-") || "my-app";
|
|
2334
|
+
let changed = !existedBefore;
|
|
2335
|
+
if (!pkg.name || typeof pkg.name !== "string") {
|
|
2336
|
+
pkg.name = fallbackName;
|
|
2337
|
+
changed = true;
|
|
2338
|
+
}
|
|
2339
|
+
if (!existedBefore && pkg.private === void 0) {
|
|
2340
|
+
pkg.private = true;
|
|
2341
|
+
changed = true;
|
|
2342
|
+
}
|
|
2343
|
+
if (!existedBefore && !pkg.type) {
|
|
2344
|
+
pkg.type = "module";
|
|
2345
|
+
changed = true;
|
|
2346
|
+
}
|
|
2347
|
+
if (!pkg.devDependencies || typeof pkg.devDependencies !== "object" || Array.isArray(pkg.devDependencies)) pkg.devDependencies = {};
|
|
2348
|
+
if (!pkg.dependencies || typeof pkg.dependencies !== "object" || Array.isArray(pkg.dependencies)) pkg.dependencies = {};
|
|
2349
|
+
const devDependencies = asRecord(pkg.devDependencies) ?? {};
|
|
2350
|
+
pkg.devDependencies = devDependencies;
|
|
2351
|
+
if (!DEPENDENCY_FIELDS.some((field) => {
|
|
2352
|
+
return typeof asRecord(pkg[field])?.void === "string";
|
|
2353
|
+
})) {
|
|
2354
|
+
devDependencies.void = voidSpec;
|
|
2355
|
+
changed = true;
|
|
2356
|
+
}
|
|
2357
|
+
if (isPnpmProject(root, pkg)) {
|
|
2358
|
+
const before = JSON.stringify(asRecord(pkg.pnpm)?.onlyBuiltDependencies ?? null);
|
|
2359
|
+
ensurePnpmBuiltDependencies(pkg);
|
|
2360
|
+
if (before !== JSON.stringify(asRecord(pkg.pnpm)?.onlyBuiltDependencies ?? null)) changed = true;
|
|
2361
|
+
}
|
|
2362
|
+
if (changed) {
|
|
2363
|
+
writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
2364
|
+
R.success(existedBefore ? "Ensured void is in package.json devDependencies" : "Created package.json with void as a devDependency");
|
|
2365
|
+
}
|
|
2366
|
+
return changed;
|
|
2367
|
+
}
|
|
2368
|
+
function detectInstructionFilePath(root, agents) {
|
|
2369
|
+
if (agents.some((a) => a.skillsDir === ".claude/skills")) return "CLAUDE.md";
|
|
2370
|
+
if (existsSync(join(root, "CLAUDE.md"))) return "CLAUDE.md";
|
|
2371
|
+
if (existsSync(join(root, "AGENTS.md"))) return "AGENTS.md";
|
|
2372
|
+
return "AGENTS.md";
|
|
2373
|
+
}
|
|
2374
|
+
async function pickAgentWhenUndetected() {
|
|
2375
|
+
const choice = await Ee({
|
|
2376
|
+
message: "Could not detect your coding agent. Which one are you using?",
|
|
2377
|
+
options: [
|
|
2378
|
+
{
|
|
2379
|
+
value: "claude-code",
|
|
2380
|
+
label: "Claude Code"
|
|
2381
|
+
},
|
|
2382
|
+
{
|
|
2383
|
+
value: "cursor",
|
|
2384
|
+
label: "Cursor"
|
|
2385
|
+
},
|
|
2386
|
+
{
|
|
2387
|
+
value: "codex",
|
|
2388
|
+
label: "Codex"
|
|
2389
|
+
},
|
|
2390
|
+
{
|
|
2391
|
+
value: "gemini-cli",
|
|
2392
|
+
label: "Gemini CLI"
|
|
2393
|
+
},
|
|
2394
|
+
{
|
|
2395
|
+
value: "generic",
|
|
2396
|
+
label: "Generic"
|
|
2397
|
+
}
|
|
2398
|
+
]
|
|
2399
|
+
});
|
|
2400
|
+
if (q(choice)) {
|
|
2401
|
+
me("Setup cancelled.");
|
|
2402
|
+
process.exit(0);
|
|
2403
|
+
}
|
|
2404
|
+
if (choice === "generic") return {
|
|
2405
|
+
instructionFilePath: "AGENTS.md",
|
|
2406
|
+
agents: []
|
|
2407
|
+
};
|
|
2408
|
+
const selected = getAgentById(choice);
|
|
2409
|
+
if (!selected) return {
|
|
2410
|
+
instructionFilePath: "AGENTS.md",
|
|
2411
|
+
agents: []
|
|
2412
|
+
};
|
|
2413
|
+
return {
|
|
2414
|
+
instructionFilePath: choice === "claude-code" ? "CLAUDE.md" : "AGENTS.md",
|
|
2415
|
+
agents: [selected]
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
async function resolveAgentSetup(root) {
|
|
2419
|
+
const detected = detectAgents(root);
|
|
2420
|
+
if (detected.length > 0) return {
|
|
2421
|
+
instructionFilePath: detectInstructionFilePath(root, detected),
|
|
2422
|
+
agents: detected
|
|
2423
|
+
};
|
|
2424
|
+
return pickAgentWhenUndetected();
|
|
2425
|
+
}
|
|
2426
|
+
function readAgentPrompt() {
|
|
2427
|
+
return readFileSync(join(getPackageDir(), "AGENT_PROMPT.md"), "utf-8");
|
|
2428
|
+
}
|
|
2429
|
+
function getOwnVersion() {
|
|
2430
|
+
return JSON.parse(readFileSync(join(getPackageDir(), "package.json"), "utf-8")).version;
|
|
2431
|
+
}
|
|
2432
|
+
const MARKER_OPEN_RE = /<!--injected-by-void-v([\d.]+)-->/;
|
|
2433
|
+
const MARKER_CLOSE = "<!--/injected-by-void-->";
|
|
2434
|
+
const MARKER_BLOCK_RE = /<!--injected-by-void-v[\d.]+-->\n[\s\S]*?<!--\/injected-by-void-->/;
|
|
2435
|
+
function injectAgentBlock(root, filePath) {
|
|
2436
|
+
const fullPath = join(root, filePath);
|
|
2437
|
+
const version = getOwnVersion();
|
|
2438
|
+
const promptContent = readAgentPrompt();
|
|
2439
|
+
const block = `${`<!--injected-by-void-v${version}-->`}\n${promptContent}\n${MARKER_CLOSE}`;
|
|
2440
|
+
if (existsSync(fullPath)) {
|
|
2441
|
+
const existing = readFileSync(fullPath, "utf-8");
|
|
2442
|
+
const match = existing.match(MARKER_OPEN_RE);
|
|
2443
|
+
if (match) {
|
|
2444
|
+
if (match[1] === version) {
|
|
2445
|
+
R.info(`${filePath} already has Void instructions (v${version})`);
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
writeFileSync(fullPath, existing.replace(MARKER_BLOCK_RE, block));
|
|
2449
|
+
R.success(`Updated Void instructions in ${filePath} (v${match[1]} → v${version})`);
|
|
2450
|
+
} else {
|
|
2451
|
+
writeFileSync(fullPath, existing + (existing.endsWith("\n") ? "\n" : "\n\n") + block + "\n");
|
|
2452
|
+
R.success(`Added Void instructions to ${filePath}`);
|
|
2453
|
+
}
|
|
2454
|
+
} else {
|
|
2455
|
+
writeFileSync(fullPath, block + "\n");
|
|
2456
|
+
R.success(`Created ${filePath} with Void instructions`);
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
function writeMcpConfigForTarget(root, target) {
|
|
2460
|
+
const fullPath = join(root, target.filePath);
|
|
2461
|
+
let existing = {};
|
|
2462
|
+
if (existsSync(fullPath)) try {
|
|
2463
|
+
existing = asRecord(JSON.parse(readFileSync(fullPath, "utf-8"))) ?? {};
|
|
2464
|
+
} catch {}
|
|
2465
|
+
const rootConfig = asRecord(existing[target.rootKey]) ?? {};
|
|
2466
|
+
existing[target.rootKey] = rootConfig;
|
|
2467
|
+
if (rootConfig["void"]) {
|
|
2468
|
+
R.info(`${target.filePath} already has void MCP config`);
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
rootConfig["void"] = {
|
|
2472
|
+
command: "npx",
|
|
2473
|
+
args: ["void", "mcp"],
|
|
2474
|
+
...target.extraFields
|
|
2475
|
+
};
|
|
2476
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
2477
|
+
writeFileSync(fullPath, JSON.stringify(existing, null, 2) + "\n");
|
|
2478
|
+
R.success(`Added void MCP server to ${target.filePath}`);
|
|
2479
|
+
}
|
|
2480
|
+
function pickMcpTarget(root, targets) {
|
|
2481
|
+
if (targets.length === 1) return targets[0];
|
|
2482
|
+
const existingTargets = targets.filter((t) => existsSync(join(root, t.filePath)));
|
|
2483
|
+
if (existingTargets.length > 0) return existingTargets[0];
|
|
2484
|
+
return targets[0];
|
|
2485
|
+
}
|
|
2486
|
+
function setupMcpConfig(root, selectedAgents) {
|
|
2487
|
+
const mcpAgents = [];
|
|
2488
|
+
const hintAgents = [];
|
|
2489
|
+
for (const agent of selectedAgents) if (agent.mcpConfig) mcpAgents.push({
|
|
2490
|
+
agent,
|
|
2491
|
+
targets: agent.mcpConfig
|
|
2492
|
+
});
|
|
2493
|
+
else if (agent.mcpHint) hintAgents.push({
|
|
2494
|
+
agent,
|
|
2495
|
+
hint: agent.mcpHint
|
|
2496
|
+
});
|
|
2497
|
+
if (selectedAgents.length === 0) {
|
|
2498
|
+
Se(JSON.stringify({ void: {
|
|
2499
|
+
command: "npx",
|
|
2500
|
+
args: ["void", "mcp"]
|
|
2501
|
+
} }, null, 2), "Add this MCP server config to your agent");
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2504
|
+
for (const { agent, hint } of hintAgents) R.info(`${agent.displayName}: ${hint}`);
|
|
2505
|
+
for (const { agent, targets } of mcpAgents) {
|
|
2506
|
+
const target = pickMcpTarget(root, targets);
|
|
2507
|
+
R.info(`${agent.displayName} MCP target: ${target.filePath}`);
|
|
2508
|
+
writeMcpConfigForTarget(root, target);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
async function runInitCommand(root, initArgs) {
|
|
2512
|
+
console.log();
|
|
2513
|
+
ge(cliTitle("init"));
|
|
2514
|
+
const targeted = Boolean(initArgs && (initArgs.tsconfig || initArgs.github || initArgs.agents));
|
|
2515
|
+
const framework = detectFramework(root);
|
|
2516
|
+
const preScaffoldState = detectProjectState(root);
|
|
2517
|
+
const detectedPagesFramework = detectPagesFramework(root);
|
|
2518
|
+
let scaffoldedPagesFramework;
|
|
2519
|
+
let scaffoldedStarterMode;
|
|
2520
|
+
let didScaffoldStarter = false;
|
|
2521
|
+
let scaffoldAddedDependencies = false;
|
|
2522
|
+
let workspaceSubpackageSkip = false;
|
|
2523
|
+
if (framework) R.info(`Detected ${framework.name} framework`);
|
|
2524
|
+
if (!targeted && !framework && isScaffoldableEmptyProject(root)) {
|
|
2525
|
+
R.info("Empty project detected. Scaffolding a Pages starter...");
|
|
2526
|
+
if (detectedPagesFramework) {
|
|
2527
|
+
scaffoldedPagesFramework = detectedPagesFramework;
|
|
2528
|
+
R.info(`Detected ${SCAFFOLD_FRAMEWORKS[detectedPagesFramework].label} adapter in package.json`);
|
|
2529
|
+
} else scaffoldedPagesFramework = await promptScaffoldFramework();
|
|
2530
|
+
const starterMode = await promptStarterMode();
|
|
2531
|
+
const scaffoldWroteDeps = scaffoldPagesApp(root, preScaffoldState.packageManager, scaffoldedPagesFramework, starterMode);
|
|
2532
|
+
const deferral = resolveWorkspaceDeferral(root, readPackageJson(root));
|
|
2533
|
+
const voidResolvable = isVoidResolvableFrom(root);
|
|
2534
|
+
if (deferral && (!voidResolvable || scaffoldWroteDeps)) {
|
|
2535
|
+
workspaceSubpackageSkip = true;
|
|
2536
|
+
const relRoot = relative(deferral.workspaceRoot, root) || ".";
|
|
2537
|
+
const dbFollowUp = starterMode === "static" ? "" : `\nthen run \`void db generate\` from ./${relRoot} to create the initial migration.`;
|
|
2538
|
+
Se(`Scaffolded the Pages starter in ./${relRoot}.\n\nRun your workspace install from ${deferral.workspaceRoot} to pick up the new dependencies.${dbFollowUp}`, "Workspace subpackage");
|
|
2539
|
+
} else if (scaffoldWroteDeps || !voidResolvable) scaffoldAddedDependencies = true;
|
|
2540
|
+
if (starterMode !== "static") scaffoldedStarterMode = starterMode;
|
|
2541
|
+
didScaffoldStarter = true;
|
|
2542
|
+
}
|
|
2543
|
+
const state = detectProjectState(root);
|
|
2544
|
+
const pagesFramework = scaffoldedPagesFramework ?? detectPagesFramework(root);
|
|
2545
|
+
if (!targeted && !didScaffoldStarter) {
|
|
2546
|
+
const existingSetup = setupExistingProject(root, framework);
|
|
2547
|
+
const voidResolvable = isVoidResolvableFrom(root);
|
|
2548
|
+
if (existingSetup.dependencyAdded || !voidResolvable) {
|
|
2549
|
+
const deferral = resolveWorkspaceDeferral(root, readPackageJson(root));
|
|
2550
|
+
if (deferral) {
|
|
2551
|
+
workspaceSubpackageSkip = true;
|
|
2552
|
+
const relRoot = relative(deferral.workspaceRoot, root) || ".";
|
|
2553
|
+
Se(`Configured Void in ./${relRoot}.\n\nRun your workspace install from ${deferral.workspaceRoot} to sync the lockfile and pick up the updated dependencies.\nIf this init flow scaffolds database files, then run \`void db generate\` from ./${relRoot}.`, "Workspace subpackage");
|
|
2554
|
+
} else scaffoldAddedDependencies = true;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
if (!targeted || initArgs?.tsconfig) {
|
|
2558
|
+
state.typescriptWanted = true;
|
|
2559
|
+
setupTsconfig(root, state, pagesFramework);
|
|
2560
|
+
}
|
|
2561
|
+
if (state.typescriptWanted) {
|
|
2562
|
+
const hasSchema = hasDrizzleSchema(root);
|
|
2563
|
+
ensureProjectTsconfig(root, void 0, hasSchema, hasSchema);
|
|
2564
|
+
}
|
|
2565
|
+
ensureGitignore(root);
|
|
2566
|
+
if (framework) setupWranglerConfig(root, framework);
|
|
2567
|
+
if (!targeted) ensureVoidWorkerCompatibilityDate(root);
|
|
2568
|
+
if (!targeted && !didScaffoldStarter) applyDatabaseSetup(root, await promptDatabaseSetup(root));
|
|
2569
|
+
if (!targeted || initArgs?.agents) {
|
|
2570
|
+
const agentSetup = await resolveAgentSetup(root);
|
|
2571
|
+
injectAgentBlock(root, agentSetup.instructionFilePath);
|
|
2572
|
+
if (agentSetup.agents.length > 0) {
|
|
2573
|
+
const { linkSkillsForSpecificAgents } = await import("./skills-ipldjlKE.mjs");
|
|
2574
|
+
linkSkillsForSpecificAgents(root, agentSetup.agents);
|
|
2575
|
+
} else R.info("Skills linking skipped for generic agent setup");
|
|
2576
|
+
setupMcpConfig(root, agentSetup.agents);
|
|
2577
|
+
}
|
|
2578
|
+
if (!targeted && !framework) if (didScaffoldStarter || state.hasRoutes || state.hasMigrations || state.hasPages) R.info("Project already has pages, routes, or migrations, skipping demo");
|
|
2579
|
+
else if (!state.typescriptWanted) {} else {
|
|
2580
|
+
const scaffold = await ue({ message: "Scaffold demo code? (migration + API route + frontend fetch)" });
|
|
2581
|
+
if (q(scaffold)) {
|
|
2582
|
+
me("Setup cancelled.");
|
|
2583
|
+
process.exit(0);
|
|
2584
|
+
}
|
|
2585
|
+
if (scaffold) {
|
|
2586
|
+
const { packageJsonMutated } = scaffoldDemo(root);
|
|
2587
|
+
const voidResolvable = isVoidResolvableFrom(root);
|
|
2588
|
+
const deferral = resolveWorkspaceDeferral(root, readPackageJson(root));
|
|
2589
|
+
if (deferral && (!voidResolvable || packageJsonMutated)) {
|
|
2590
|
+
workspaceSubpackageSkip = true;
|
|
2591
|
+
const relRoot = relative(deferral.workspaceRoot, root) || ".";
|
|
2592
|
+
Se(`Scaffolded the starter demo in ./${relRoot}.\n\n${voidResolvable ? `\`void\` is already hoisted at an ancestor node_modules, but \`./${relRoot}/package.json\` was updated.\nRun your workspace install from ${deferral.workspaceRoot} to sync the lockfile,` : `Run your workspace install from ${deferral.workspaceRoot} to pick up the new \`void\` dep,`}\nthen run \`void db generate\` from ./${relRoot} to create the initial migration.`, "Workspace subpackage");
|
|
2593
|
+
} else if (!voidResolvable || packageJsonMutated) scaffoldAddedDependencies = true;
|
|
2594
|
+
scaffoldedStarterMode = "sqlite";
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
const githubStepSkipForSubpackage = isInsideWorkspaceSubpackage(root);
|
|
2598
|
+
if ((!targeted || initArgs?.github) && !githubStepSkipForSubpackage) if (existsSync(join(root, ".github", "workflows", "deploy.yml"))) R.warn("deploy.yml already exists, skipping");
|
|
2599
|
+
else if (targeted) setupGithubActions(root, state);
|
|
2600
|
+
else {
|
|
2601
|
+
const setupGha = await ue({ message: "Create .github/workflows/deploy.yml to deploy on pushes to main?" });
|
|
2602
|
+
if (q(setupGha)) {
|
|
2603
|
+
me("Setup cancelled.");
|
|
2604
|
+
process.exit(0);
|
|
2605
|
+
}
|
|
2606
|
+
if (setupGha) setupGithubActions(root, state);
|
|
2607
|
+
}
|
|
2608
|
+
else if (githubStepSkipForSubpackage && (initArgs?.github || !targeted)) R.info("Skipping GitHub Actions setup: workflow files must live at the repo root, not in a workspace subpackage.");
|
|
2609
|
+
if (!targeted) {
|
|
2610
|
+
const scaffoldResult = maybeScaffoldEnvTs(root);
|
|
2611
|
+
if (scaffoldResult.written) {
|
|
2612
|
+
const rel = relative(root, scaffoldResult.written) || "env.ts";
|
|
2613
|
+
if (scaffoldResult.entries.length > 0) R.success(`Scaffolded ${rel} from your .env files — review inferred types before running dev.`);
|
|
2614
|
+
else R.success(`Created empty ${rel} — add your env keys with \`defineEnv({...})\`.`);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
if (scaffoldAddedDependencies) installProjectDependencies(root, state.packageManager);
|
|
2618
|
+
if (scaffoldedStarterMode && !workspaceSubpackageSkip) generateStarterMigration(root, scaffoldedStarterMode);
|
|
2619
|
+
if (!targeted) await maybeSetupDeployProject(root);
|
|
2620
|
+
if (workspaceSubpackageSkip) if (scaffoldedStarterMode) ye("Run your workspace install, then `void db generate` to finish setup.");
|
|
2621
|
+
else ye("Run your workspace install to finish setup.");
|
|
2622
|
+
else ye(`Run \`${getDevCommand(root, framework)}\` to start developing`);
|
|
2623
|
+
}
|
|
2624
|
+
//#endregion
|
|
2625
|
+
export { parseInitArgs, runInitCommand };
|