From 1eeeded79500eaffdd499c03b1a07a6f38bfb984 Mon Sep 17 00:00:00 2001
From: Yangshun
Date: Sun, 25 Oct 2026 16:59:16 +0800
Subject: [PATCH 1/5] feat(init): refresh Pages starter templates
---
.github/scripts/packed-consumer.mjs | 77 +++-
TESTING.md | 4 +-
packages/void/package.json | 1 +
packages/void/src/cli/starter-ui.ts | 397 ++++++++++++++++++++
packages/void/src/cli/template.ts | 429 ++++++++++++----------
packages/void/templates/assets/icons.svg | 49 +++
packages/void/templates/assets/react.svg | 1 +
packages/void/templates/assets/solid.svg | 1 +
packages/void/templates/assets/stack.svg | 35 ++
packages/void/templates/assets/svelte.svg | 1 +
packages/void/templates/assets/void.svg | 5 +
packages/void/templates/assets/vue.svg | 1 +
packages/void/test/unit/init.test.ts | 46 ++-
13 files changed, 840 insertions(+), 207 deletions(-)
create mode 100644 packages/void/src/cli/starter-ui.ts
create mode 100644 packages/void/templates/assets/icons.svg
create mode 100644 packages/void/templates/assets/react.svg
create mode 100644 packages/void/templates/assets/solid.svg
create mode 100644 packages/void/templates/assets/stack.svg
create mode 100644 packages/void/templates/assets/svelte.svg
create mode 100644 packages/void/templates/assets/void.svg
create mode 100644 packages/void/templates/assets/vue.svg
diff --git a/.github/scripts/packed-consumer.mjs b/.github/scripts/packed-consumer.mjs
index 94778ffe0..093e784d6 100644
--- a/.github/scripts/packed-consumer.mjs
+++ b/.github/scripts/packed-consumer.mjs
@@ -116,6 +116,67 @@ async function run(command, args, cwd, env = environment) {
}
}
+async function verifyStarterForm(fixture, cli, vite, framework, label, browser) {
+ await run(process.execPath, [cli, 'db', 'reset', '--yes'], fixture);
+ const task = launch(
+ process.execPath,
+ [vite, 'dev', '--host', '127.0.0.1', '--port', '0', '--strictPort'],
+ fixture,
+ );
+ const page = await browser.newPage();
+ const diagnostics = collectBrowserDiagnostics(page);
+ try {
+ const deadline = Date.now() + 90_000;
+ let base;
+ while (Date.now() < deadline) {
+ if (task.child.exitCode !== null) {
+ throw new Error(task.failure());
+ }
+ base ??= packedServerUrl(task.output());
+ if (base) {
+ try {
+ const response = await fetch(base, { signal: AbortSignal.timeout(2000) });
+ if (response.ok) {
+ break;
+ }
+ } catch {
+ /* server is starting */
+ }
+ }
+ await new Promise((resolve) => setTimeout(resolve, 200));
+ }
+ assert(base, task.failure());
+ await page.goto(base);
+ await page.locator('#app[data-hydrated="true"]').waitFor();
+ await page.getByRole('heading', { name: 'Welcome to Void', exact: true }).waitFor();
+ await page.getByText('No messages yet.', { exact: false }).waitFor();
+ const input = page.getByRole('textbox', { name: 'Message', exact: true });
+ const submit = page.getByRole('button', { name: 'Add message', exact: true });
+ await input.fill(' ');
+ await submit.click();
+ await page.getByRole('alert').waitFor();
+ assert.equal(await page.locator('.messages li').count(), 0);
+ const message = `Message from the ${framework} starter`;
+ await input.fill(message);
+ await submit.click();
+ await page.locator('.messages').getByText(message, { exact: true }).waitFor();
+ await page.reload();
+ await page.locator('.messages').getByText(message, { exact: true }).waitFor();
+ assert.equal(await page.locator('.messages li').count(), 1);
+ console.log(
+ `Passed packed ${label} starter: empty state, validation, form submission, and persisted messages`,
+ );
+ } catch (error) {
+ throw new Error(
+ `Packed ${label} starter form failed:\n${String(error.stack ?? error)}\n${diagnostics()}\n${task.failure()}`,
+ { cause: error },
+ );
+ } finally {
+ await page.close();
+ await task.stop();
+ }
+}
+
const packages = [];
for (const entry of readdirSync(join(root, 'packages'), { withFileTypes: true })) {
if (!entry.isDirectory()) {
@@ -308,6 +369,18 @@ try {
);
await run(process.execPath, [cli, 'prepare'], fixture);
await run(manager, manager === 'npm' ? ['ci'] : ['install', '--frozen-lockfile'], fixture);
+ const vite = join(
+ fixture,
+ useVitePlus ? 'node_modules/vite-plus/bin/vp' : 'node_modules/vite/bin/vite.js',
+ );
+ if (browser) {
+ await run(
+ process.execPath,
+ [join(fixture, 'node_modules/typescript/bin/tsc'), '--noEmit'],
+ fixture,
+ );
+ await verifyStarterForm(fixture, cli, vite, framework, label, browser);
+ }
const extension = framework === 'vue' ? 'vue' : framework === 'svelte' ? 'svelte' : 'tsx';
writeFileSync(join(fixture, `pages/index.${extension}`), pages[framework]);
writeFileSync(join(fixture, `pages/next.${extension}`), pages[framework]);
@@ -331,10 +404,6 @@ try {
);
mkdirSync(join(fixture, 'public'), { recursive: true });
writeFileSync(join(fixture, 'public/packed.txt'), label);
- const vite = join(
- fixture,
- useVitePlus ? 'node_modules/vite-plus/bin/vp' : 'node_modules/vite/bin/vite.js',
- );
await run(process.execPath, [vite, 'build'], fixture);
await run(
process.execPath,
diff --git a/TESTING.md b/TESTING.md
index 2708d2bca..ec7406ff2 100644
--- a/TESTING.md
+++ b/TESTING.md
@@ -214,7 +214,9 @@ workspace links can hide. They do not publish packages or deploy applications.
Smoke coverage checks package contents and exports, real initialization, CLI help, a frozen
reinstall, public Vite builds, TypeScript, and development HTTP requests for pages, assets, and
KV. It uses no browser. Full coverage adds Vue, Svelte, Solid, Markdown, production preview,
-and browser hydration/navigation. Superseded PR/main runs are cancelled; release runs are not.
+and browser hydration/navigation. It also checks each generated starter's empty state,
+validation, form submission, and message persistence against local D1 before replacing its pages
+with the broader runtime fixtures. Superseded PR/main runs are cancelled; release runs are not.
The selector reads all PR file pages or the complete pushed commit range. Renames consider both
paths. Incomplete or unavailable change lists fall back to full coverage.
diff --git a/packages/void/package.json b/packages/void/package.json
index b69637f34..e7f51bc84 100644
--- a/packages/void/package.json
+++ b/packages/void/package.json
@@ -20,6 +20,7 @@
"docs",
"schema.json",
"skills",
+ "templates",
"AGENTS_TEMPLATE.md",
"README.md",
"sandbox.Dockerfile"
diff --git a/packages/void/src/cli/starter-ui.ts b/packages/void/src/cli/starter-ui.ts
new file mode 100644
index 000000000..467bc1ab1
--- /dev/null
+++ b/packages/void/src/cli/starter-ui.ts
@@ -0,0 +1,397 @@
+export const STARTER_STYLES: string = `:root {
+ --text: rgb(22 23 29 / 70%);
+ --text-h: #16171d;
+ --bg: #fff;
+ --border: #e5e4e7;
+ --code-bg: #f2f1f4;
+ --accent: #6c3bff;
+ --error: #b42318;
+ --social-bg: #f2f1f4;
+ --shadow:
+ rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
+
+ --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
+ --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
+ --mono: ui-monospace, Consolas, monospace;
+
+ font: 18px/145% var(--sans);
+ letter-spacing: 0.18px;
+ color-scheme: light dark;
+ color: var(--text);
+ background: var(--bg);
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ @media (max-width: 1024px) {
+ font-size: 16px;
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --text: rgb(255 255 255 / 70%);
+ --text-h: #fff;
+ --bg: #16171d;
+ --border: #3b3440;
+ --code-bg: #3b3440;
+ --accent: #b39aff;
+ --error: #fda29b;
+ --social-bg: #3b3440;
+ --shadow:
+ rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
+ }
+
+}
+
+.starter {
+ width: 1126px;
+ max-width: 100%;
+ margin: 0 auto;
+ text-align: center;
+ border-inline: 1px solid var(--border);
+ min-height: 100svh;
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+}
+
+h1,
+h2 {
+ font-family: var(--heading);
+ font-weight: 500;
+ color: var(--text-h);
+}
+
+h1 {
+ font-size: 56px;
+ letter-spacing: -1.68px;
+ margin: 32px 0;
+ @media (max-width: 1024px) {
+ font-size: 36px;
+ margin: 20px 0;
+ }
+}
+h2 {
+ font-size: 24px;
+ line-height: 118%;
+ letter-spacing: -0.24px;
+ margin: 0 0 8px;
+ @media (max-width: 1024px) {
+ font-size: 20px;
+ }
+}
+p {
+ margin: 0;
+}
+
+code {
+ font-family: var(--mono);
+ display: inline-flex;
+ border-radius: 4px;
+ color: var(--text-h);
+ font-size: 15px;
+ line-height: 135%;
+ padding: 4px 8px;
+ background: var(--code-bg);
+}
+
+.hero {
+ position: relative;
+
+ .base,
+ .framework,
+ .void {
+ inset-inline: 0;
+ margin: 0 auto;
+ }
+
+ .base {
+ width: 170px;
+ position: relative;
+ z-index: 0;
+ }
+
+ .framework,
+ .void {
+ position: absolute;
+ }
+
+ .framework {
+ z-index: 1;
+ top: 34px;
+ height: 28px;
+ transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
+ scale(1.4);
+ }
+
+ .void {
+ z-index: 0;
+ top: 107px;
+ height: 22px;
+ width: auto;
+ transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
+ scale(0.8);
+ }
+}
+
+#center {
+ display: flex;
+ flex-direction: column;
+ gap: 25px;
+ padding-bottom: 24px;
+ place-content: center;
+ place-items: center;
+ flex-grow: 1;
+
+ @media (max-width: 1024px) {
+ padding: 32px 20px 24px;
+ gap: 18px;
+ }
+}
+
+.starter-intro {
+ width: min(720px, 100%);
+ padding-inline: 20px;
+ box-sizing: border-box;
+
+ a {
+ color: var(--accent);
+ }
+
+ @media (max-width: 1024px) {
+ padding-inline: 0;
+ }
+}
+
+.starter-description {
+ display: grid;
+ gap: 12px;
+
+ > p:nth-child(2) {
+ text-wrap: balance;
+ }
+}
+
+.starter-demo {
+ border-top: 1px solid var(--border);
+ padding: 32px;
+ text-align: left;
+
+ @media (max-width: 1024px) {
+ padding: 24px 20px;
+ }
+}
+
+.demo-content {
+ max-width: 640px;
+ margin: 0 auto;
+ display: grid;
+ gap: 24px;
+
+ h2 {
+ margin: 0;
+ }
+}
+
+.messages {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: grid;
+ gap: 12px;
+
+ li {
+ padding: 12px 16px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ overflow-wrap: anywhere;
+ }
+
+ strong {
+ display: block;
+ color: var(--text-h);
+ font-weight: 500;
+ }
+
+ small {
+ font-size: 14px;
+ }
+}
+
+.message-form {
+ display: grid;
+ gap: 12px;
+
+ label {
+ color: var(--text-h);
+ }
+
+ input,
+ button {
+ font: inherit;
+ color: var(--text-h);
+ background: var(--social-bg);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 10px 12px;
+
+ &:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ }
+ }
+
+ input {
+ flex: 1;
+ min-width: 0;
+ }
+
+ button {
+ cursor: pointer;
+
+ &:hover {
+ border-color: var(--accent);
+ }
+
+ &:disabled {
+ cursor: wait;
+ opacity: 0.6;
+ }
+ }
+}
+
+.message-fields {
+ display: flex;
+ gap: 8px;
+
+ @media (max-width: 480px) {
+ flex-direction: column;
+ }
+}
+
+.message-form [data-void-form-error] {
+ color: var(--error);
+ font-size: 14px;
+}
+
+#next-steps {
+ display: flex;
+ border-top: 1px solid var(--border);
+ text-align: left;
+
+ & > div {
+ flex: 1 1 0;
+ padding: 32px;
+ @media (max-width: 1024px) {
+ padding: 24px 20px;
+ }
+ }
+
+ .icon {
+ color: var(--accent);
+ margin-bottom: 16px;
+ width: 22px;
+ height: 22px;
+ }
+
+ @media (max-width: 1024px) {
+ flex-direction: column;
+ text-align: center;
+ }
+}
+
+#docs {
+ border-right: 1px solid var(--border);
+
+ @media (max-width: 1024px) {
+ border-right: none;
+ border-bottom: 1px solid var(--border);
+ }
+}
+
+#next-steps ul {
+ list-style: none;
+ padding: 0;
+ display: flex;
+ gap: 8px;
+ margin: 32px 0 0;
+
+ .logo {
+ width: 36px;
+ height: auto;
+ }
+
+ a {
+ color: var(--text-h);
+ font-size: 16px;
+ border-radius: 6px;
+ background: var(--social-bg);
+ display: flex;
+ padding: 6px 12px;
+ align-items: center;
+ gap: 8px;
+ text-decoration: none;
+ transition: box-shadow 0.3s;
+
+ &:hover {
+ box-shadow: var(--shadow);
+ }
+ .button-icon {
+ height: 18px;
+ width: 18px;
+ }
+ }
+
+ @media (max-width: 1024px) {
+ margin-top: 20px;
+ flex-wrap: wrap;
+ justify-content: center;
+
+ li {
+ flex: 1 1 calc(50% - 8px);
+ }
+
+ a {
+ width: 100%;
+ justify-content: center;
+ box-sizing: border-box;
+ }
+ }
+}
+
+#spacer {
+ height: 88px;
+ border-top: 1px solid var(--border);
+ @media (max-width: 1024px) {
+ height: 48px;
+ }
+}
+
+.ticks {
+ position: relative;
+ width: 100%;
+
+ &::before,
+ &::after {
+ content: '';
+ position: absolute;
+ top: -4.5px;
+ border: 5px solid transparent;
+ }
+
+ &::before {
+ left: 0;
+ border-left-color: var(--border);
+ }
+ &::after {
+ right: 0;
+ border-right-color: var(--border);
+ }
+}
+`;
diff --git a/packages/void/src/cli/template.ts b/packages/void/src/cli/template.ts
index 013aa8ac8..6db68ec8e 100644
--- a/packages/void/src/cli/template.ts
+++ b/packages/void/src/cli/template.ts
@@ -1,3 +1,8 @@
+import { readFileSync } from 'node:fs';
+import { join } from 'pathslash';
+import { getPackageDir } from './agents.ts';
+import { STARTER_STYLES } from './starter-ui.ts';
+
export const SCAFFOLD_GITIGNORE_LINES = [
'node_modules',
'dist',
@@ -59,6 +64,7 @@ export const SCAFFOLD_FRAMEWORK_OPTIONS = [
type ScaffoldFrameworkConfig = {
label: string;
+ docsUrl: string;
adapterPackage: string;
adapterImport: string;
adapterPlugin: string;
@@ -71,6 +77,7 @@ type ScaffoldFrameworkConfig = {
export const SCAFFOLD_FRAMEWORKS: Record = {
react: {
label: 'React',
+ docsUrl: 'https://react.dev/',
adapterPackage: '@void/react',
adapterImport: 'voidReact',
adapterPlugin: '@void/react/plugin',
@@ -90,6 +97,7 @@ export const SCAFFOLD_FRAMEWORKS: Record = {
+ const assets = join(getPackageDir(), 'templates', 'assets');
+ const files: PagesStarterTemplate['files'] = {
'vite.config.ts': getViteConfig(framework, config),
[`pages/index.${config.pageExtension}`]: getIndexPage(framework, starterMode),
+ 'pages/style.css': STARTER_STYLES,
+ 'pages/vite-env.d.ts': '/// \n',
+ 'public/stack.svg': readFileSync(join(assets, 'stack.svg'), 'utf8'),
+ 'public/void.svg': readFileSync(join(assets, 'void.svg'), 'utf8'),
+ [`public/${framework}.svg`]: readFileSync(join(assets, `${framework}.svg`), 'utf8'),
+ 'public/icons.svg': readFileSync(join(assets, 'icons.svg'), 'utf8'),
};
if (starterMode !== 'static') {
@@ -322,218 +339,236 @@ export default defineSeed(async ({ db, schema }) => {
}
function getIndexPage(framework: ScaffoldFramework, starterMode: ScaffoldStarterMode): string {
- if (starterMode === 'static') {
- switch (framework) {
- case 'react':
- return `export default function HomePage() {
- return (
-
- Welcome to Void
- This starter is running in Pages mode with no database or server code yet.
-
- Add pages/*.server.ts, routes/, or void/db when
- you want to grow into full-stack features.
-
-
- );
-}
-`;
- case 'vue':
- return `
-
- Welcome to Void
- This starter is running in Pages mode with no database or server code yet.
-
- Add pages/*.server.ts, routes/, or void/db when you
- want to grow into full-stack features.
-
-
-
-`;
- case 'svelte':
- return `
- Welcome to Void
- This starter is running in Pages mode with no database or server code yet.
-
- Add pages/*.server.ts, routes/, or void/db when you
- want to grow into full-stack features.
-
-
-`;
- case 'solid':
- return `export default function HomePage() {
- return (
-
- Welcome to Void
- This starter is running in Pages mode with no database or server code yet.
-
- Add pages/*.server.ts, routes/, or void/db when
- you want to grow into full-stack features.
-
-
- );
-}
-`;
- }
- }
+ const { label, docsUrl } = SCAFFOLD_FRAMEWORKS[framework];
+ const hasDatabase = starterMode !== 'static';
+ const description =
+ starterMode === 'static'
+ ? `This starter is running in Pages mode with no database or server code yet.
+
+ Add pages/*.server.ts, routes/, or void/db when
+ you want to grow into full-stack features.
+
`
+ : `
+ This starter is running in Pages mode. The list below is loaded from your database in
+ pages/index.server.ts.
+
+API example: /api/hello
`;
+ const body = `
+
+
+
+
Welcome to Void
+
+${indent(description, 8)}
+
+
+ ${hasDatabase ? `\n
\n${indent(getMessagesDemo(framework), 2)}` : ''}
+
+
+
+
+
+
+
Documentation
+
Your questions, answered
+
+
+
+
+
+
+
Connect with us
+
Join the Void community
+
+
+
+
+
+ `;
switch (framework) {
case 'react':
- return `import { Form, useForm } from "${SCAFFOLD_FRAMEWORKS[framework].adapterPackage}";
+ case 'solid': {
+ const jsx = framework === 'react' ? body.replaceAll('class=', 'className=') : body;
+ const imports = hasDatabase
+ ? `${framework === 'solid' ? 'import { For, Show } from "solid-js";\n' : ''}import { Form, useForm } from "${SCAFFOLD_FRAMEWORKS[framework].adapterPackage}";
import type { Props } from "./index.server";
-
-export default function HomePage({ messages }: Props) {
- const form = useForm("/", { text: "" });
- return (
-
- Welcome to Void
-
- This starter is running in Pages mode. The list below is loaded from your database in
- pages/index.server.ts.
-
-
- API example: /api/hello
-
- {messages.length === 0 ? (
-
- No messages yet. Run void db seed to load the sample row from
- db/seed.ts.
-
- ) : (
-
- {messages.map((message) => (
-
- {message.text}
-
- {String(message.createdAt)}
-
- ))}
-
- )}
-
-
+`
+ : '';
+ const props = hasDatabase
+ ? framework === 'react'
+ ? '{ messages }: Props'
+ : 'props: Props'
+ : '';
+ return `${imports}import "./style.css";
+
+export default function HomePage(${props}) {
+${hasDatabase ? ' const form = useForm("/", { text: "" });\n\n' : ''} return (
+${indent(jsx, 4)}
);
}
`;
+ }
case 'vue':
return `
+${hasDatabase ? 'import { Form, useForm } from "@void/vue";\nimport type { Props } from "./index.server";\n' : ''}import "./style.css";
+${hasDatabase ? '\ndefineProps();\nconst form = useForm("/", { text: "" });\n' : ''}
-
- Welcome to Void
-
- This starter is running in Pages mode. The list below is loaded from your database in
- pages/index.server.ts.
-
-
- API example: /api/hello
-
-
- No messages yet. Run void db seed to load the sample row from
- db/seed.ts.
-
-
-
- {{ message.text }}
-
- {{ String(message.createdAt) }}
-
-
-
-
+${indent(body, 2)}
`;
case 'svelte':
return `
-
-
- Welcome to Void
-
- This starter is running in Pages mode. The list below is loaded from your database in
- pages/index.server.ts.
-
-
- API example: /api/hello
-
- {#if messages.length === 0}
-
- No messages yet. Run void db seed to load the sample row from
- db/seed.ts.
-
- {:else}
-
- {#each messages as message (message.id)}
-
- {message.text}
-
- {String(message.createdAt)}
-
- {/each}
-
- {/if}
-
-
-`;
- case 'solid':
- return `import { Form, useForm } from "${SCAFFOLD_FRAMEWORKS[framework].adapterPackage}";
-import type { Props } from "./index.server";
+${hasDatabase ? ' import { Form, useForm } from "@void/svelte";\n import type { Props } from "./index.server";\n' : ''} import "./style.css";
+${hasDatabase ? '\n let { messages }: Props = $props();\n const form = useForm("/", { text: "" });\n' : ''}
-export default function HomePage(props: Props) {
- const form = useForm("/", { text: "" });
- return (
-
- Welcome to Void
-
- This starter is running in Pages mode. The list below is loaded from your database in
- pages/index.server.ts.
-
-
- API example: /api/hello
-
- {props.messages.length === 0 ? (
-
- No messages yet. Run void db seed to load the sample row from
- db/seed.ts.
-
- ) : (
-
- {props.messages.map((message) => (
-
- {message.text}
-
- {String(message.createdAt)}
-
- ))}
-
- )}
-
-
- );
-}
+${body}
`;
}
}
+
+function getMessagesDemo(framework: ScaffoldFramework): string {
+ const empty = `
+ No messages yet. Run void db seed to load the sample row from
+ db/seed.ts.
+
`;
+ const content = {
+ react: `{messages.length === 0 ? (
+${indent(empty, 2)}
+) : (
+
+ {messages.map((message) => (
+
+ {message.text}
+ {String(message.createdAt)}
+
+ ))}
+
+)}
+`,
+ vue: `
+${indent(empty, 2)}
+
+
+
+ {{ message.text }}
+ {{ String(message.createdAt) }}
+
+
+`,
+ svelte: `{#if messages.length === 0}
+${indent(empty, 2)}
+{:else}
+
+ {#each messages as message (message.id)}
+
+ {message.text}
+ {String(message.createdAt)}
+
+ {/each}
+
+{/if}
+`,
+ solid: ` 0} fallback={
+${indent(empty, 2)}
+}>
+
+ {(message) => (
+
+ {message.text}
+ {String(message.createdAt)}
+
+ )}
+
+
+`,
+ }[framework];
+ return `
+
+
Messages
+${indent(content, 4)}
+
+`;
+}
+
+function indent(source: string, spaces: number): string {
+ return source
+ .split('\n')
+ .map((line) => ' '.repeat(spaces) + line)
+ .join('\n');
+}
diff --git a/packages/void/templates/assets/icons.svg b/packages/void/templates/assets/icons.svg
new file mode 100644
index 000000000..c54550d94
--- /dev/null
+++ b/packages/void/templates/assets/icons.svg
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/void/templates/assets/react.svg b/packages/void/templates/assets/react.svg
new file mode 100644
index 000000000..6c87de9bb
--- /dev/null
+++ b/packages/void/templates/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/void/templates/assets/solid.svg b/packages/void/templates/assets/solid.svg
new file mode 100644
index 000000000..025aa303c
--- /dev/null
+++ b/packages/void/templates/assets/solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/void/templates/assets/stack.svg b/packages/void/templates/assets/stack.svg
new file mode 100644
index 000000000..cc09ed4b7
--- /dev/null
+++ b/packages/void/templates/assets/stack.svg
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/void/templates/assets/svelte.svg b/packages/void/templates/assets/svelte.svg
new file mode 100644
index 000000000..c5e08481f
--- /dev/null
+++ b/packages/void/templates/assets/svelte.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/void/templates/assets/void.svg b/packages/void/templates/assets/void.svg
new file mode 100644
index 000000000..7da47181c
--- /dev/null
+++ b/packages/void/templates/assets/void.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/packages/void/templates/assets/vue.svg b/packages/void/templates/assets/vue.svg
new file mode 100644
index 000000000..770e9d333
--- /dev/null
+++ b/packages/void/templates/assets/vue.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/void/test/unit/init.test.ts b/packages/void/test/unit/init.test.ts
index b35c3e13d..c6e12f5f3 100644
--- a/packages/void/test/unit/init.test.ts
+++ b/packages/void/test/unit/init.test.ts
@@ -1327,7 +1327,32 @@ describe('isScaffoldableEmptyProject', () => {
const reactViteConfig = readFileSync(join(root, 'vite.config.ts'), 'utf-8');
expect(reactViteConfig).toContain('from "@void/react/plugin"');
expect(reactViteConfig).toContain('voidReact({ react: { compiler: true } })');
- expect(readFileSync(join(root, 'pages', 'index.tsx'), 'utf-8')).toContain('Welcome to Void');
+ const page = readFileSync(join(root, 'pages', 'index.tsx'), 'utf-8');
+ expect(page).toContain('Welcome to Void');
+ expect(page).toContain('The list below is loaded from your database in');
+ expect(page).toContain('href="/api/hello"');
+ expect(page).toContain('void db seed');
+ expect(page).not.toContain('no database or server code yet');
+ expect(page).not.toContain('Void +');
+ expect(page).toContain('src="/react.svg"');
+ expect(page).toContain('import "./style.css"');
+ expect(page).toContain('messages.map');
+ expect(page).toContain('useForm');
+ expect(page).toContain('`
: `
- This starter is running in Pages mode. The list below is loaded from your database in
- pages/index.server.ts.
+ This starter is running in Pages mode. The list below is loaded from your database in pages/index.server.ts.
API example: /api/hello
`;
const body = `
@@ -378,8 +377,7 @@ ${indent(description, 8)}
Connect with us
+Join the Void community
++-
+
+
+ GitHub
+
+
+ -
+
+
+ Discord
+
+
+ -
+
+
+ X.com
+
+
+ -
+
+
+ Bluesky
+
+
+
+