Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions packages/void/src/cli/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2613,22 +2613,33 @@ export const posts = sqliteTable("posts", {
`;
}

function demoFiles(root: string): { schema: string; route: string; client: string } {
const paths = readProjectPaths(root);
return {
schema: paths.schemaFile,
route: join(paths.routesDir, 'api', 'posts.ts'),
client: join(paths.ssrDir, 'demo.ts'),
};
}

/**
* Scaffolds the "SQL → types → handler → fetch" demo into an existing project.
* `setupExistingProject` has already declared `void` in `package.json`.
* The caller skips the demo when any of its files exists, so these writes never replace user code.
*/
function scaffoldDemo(root: string, dialect: DatabaseDialect): void {
const devCommand = getDevCommand(root);
const paths = readProjectPaths(root);
const files = demoFiles(root);

mkdirSync(paths.dbDir, { recursive: true });
writeFileSync(paths.schemaFile, demoSchemaSource(dialect));
writeFileSync(files.schema, demoSchemaSource(dialect));
log.step('Created db/schema.ts');

// API route
mkdirSync(join(paths.routesDir, 'api'), { recursive: true });
writeFileSync(
join(paths.routesDir, 'api', 'posts.ts'),
files.route,
`import { defineHandler } from "void";
import { db } from "void/db";
import { posts } from "@schema";
Expand All @@ -2645,7 +2656,7 @@ export const GET = defineHandler(async () => {
// Frontend demo
mkdirSync(paths.ssrDir, { recursive: true });
writeFileSync(
join(paths.ssrDir, 'demo.ts'),
files.client,
`import { fetch } from "void/client";

// Fully typed: response type is inferred from the GET handler's return type,
Expand Down Expand Up @@ -2895,8 +2906,14 @@ export async function runInitCommand(root: string, initArgs?: InitArgs): Promise

// Demo code only in the full flow, and not for meta-frameworks.
if (!targeted && !framework) {
const existingDemoFiles = Object.values(demoFiles(root)).filter((file) => existsSync(file));
if (didScaffoldStarter || state.hasRoutes || state.hasMigrations || state.hasPages) {
log.info('Project already has pages, routes, or migrations, skipping demo');
} else if (hasDrizzleSchema(root)) {
log.info('Project already has a database schema, skipping demo');
} else if (existingDemoFiles.length > 0) {
const names = existingDemoFiles.map((file) => relative(root, file)).join(', ');
log.info(`Project already has ${names}, skipping demo`);
} else {
const scaffold = await confirm({
message: 'Scaffold demo code? (migration + API route + frontend fetch)',
Expand Down
46 changes: 46 additions & 0 deletions packages/void/test/unit/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3574,6 +3574,52 @@ export default config;
},
);

it.each([
['db/schema.ts', 'Project already has a database schema, skipping demo'],
['src/demo.ts', 'Project already has src/demo.ts, skipping demo'],
])('keeps an existing %s and skips the whole demo', async (existingFile, message) => {
const root = mkdtempSync(join(tmpdir(), 'void-init-'));
try {
writeExistingViteApp(root, { void: '^0.0.1' });
const authored = 'export const authored = "keep me";\n';
mkdirSync(join(root, existingFile, '..'), { recursive: true });
writeFileSync(join(root, existingFile), authored);
mockConfirm.mockImplementation((options: { message?: string }) =>
String(options.message).startsWith('Scaffold demo code?'),
);

await runInitCommand(root);

expect(readFileSync(join(root, existingFile), 'utf-8')).toBe(authored);
expect(mockLogInfo).toHaveBeenCalledWith(message);
const prompts = mockConfirm.mock.calls.map(([options]) => String(options?.message));
expect(prompts.some((prompt) => prompt.startsWith('Scaffold demo code?'))).toBe(false);
for (const demoFile of ['db/schema.ts', 'routes/api/posts.ts', 'src/demo.ts']) {
if (demoFile !== existingFile) {
expect(existsSync(join(root, demoFile))).toBe(false);
}
}
expect(drizzleGenerateCalls()).toHaveLength(0);
} finally {
rmSync(root, { recursive: true, force: true });
}
});

it('scaffolds every demo file in a project without a schema', async () => {
const root = mkdtempSync(join(tmpdir(), 'void-init-'));
try {
writeExistingViteApp(root, { void: '^0.0.1' });

await runInitWithDemo(root);

for (const demoFile of ['db/schema.ts', 'routes/api/posts.ts', 'src/demo.ts']) {
expect(existsSync(join(root, demoFile))).toBe(true);
}
} finally {
rmSync(root, { recursive: true, force: true });
}
});

it('runs starter migration with injected PnP NODE_OPTIONS in a Yarn Berry PnP project', async () => {
const root = mkdtempSync(join(tmpdir(), 'void-init-'));
try {
Expand Down
Loading