anhedral 0.1.1 → 0.1.4

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.
@@ -1,2 +1,2 @@
1
1
  import type { ProjectOptions } from '../scaffold.js';
2
- export declare function scaffoldBackend(root: string, { projectName, displayName, frontendUrl }: ProjectOptions): Promise<void>;
2
+ export declare function scaffoldBackend(root: string, { projectName, displayName, frontendUrl, skipInstall }: ProjectOptions): Promise<void>;
@@ -1,9 +1,12 @@
1
1
  import path from 'node:path';
2
- import { writeFile, exec } from '../util.js';
2
+ import { writeFile, execCommand } from '../util.js';
3
+ import { anhedralPrint } from '../print.js';
3
4
  import { getBackendInstallCommands } from '../commands.js';
4
- export async function scaffoldBackend(root, { projectName, displayName, frontendUrl }) {
5
- const dir = path.join(root, 'backend');
6
- // writeFile creates parent dirs automatically via mkdirSync
5
+ import { BACKEND_DEPENDENCIES } from '../dependencies.js';
6
+ export async function scaffoldBackend(root, { projectName, displayName, frontendUrl, skipInstall }) {
7
+ const dir = path.join(root, 'apps/api');
8
+ anhedralPrint.section('Backend (Fastify)');
9
+ anhedralPrint.step('Writing backend source files');
7
10
  writePackageJson(dir, projectName);
8
11
  writeTsConfig(dir);
9
12
  writeEslintConfig(dir);
@@ -12,7 +15,6 @@ export async function scaffoldBackend(root, { projectName, displayName, frontend
12
15
  writeEnvFile(dir, frontendUrl);
13
16
  writeDrizzleConfig(dir);
14
17
  writeVitestConfig(dir);
15
- writeVercelJson(dir);
16
18
  writeTypes(dir);
17
19
  writeDbFiles(dir);
18
20
  writeErrorFiles(dir);
@@ -23,10 +25,19 @@ export async function scaffoldBackend(root, { projectName, displayName, frontend
23
25
  writeRoutes(dir, displayName);
24
26
  writeAppAndIndex(dir, displayName);
25
27
  writeVercelEntry(dir);
28
+ writeVercelConfig(dir);
26
29
  writeTestFiles(dir);
30
+ anhedralPrint.done('Backend source files written');
31
+ if (skipInstall) {
32
+ anhedralPrint.info('Skipping backend dependency install (--skip-install)');
33
+ anhedralPrint.info('Run after init: pnpm install');
34
+ return;
35
+ }
36
+ anhedralPrint.step('Installing backend dependencies');
27
37
  for (const command of getBackendInstallCommands()) {
28
- exec(command.cmd, dir);
38
+ execCommand(command.command, command.args, dir);
29
39
  }
40
+ anhedralPrint.done('Backend dependencies installed');
30
41
  }
31
42
  // ═══════════════════════════════════════════════════════════════════════════
32
43
  // Config files
@@ -42,7 +53,7 @@ function writePackageJson(dir, projectName) {
42
53
  build: 'pnpm typecheck',
43
54
  typecheck: 'tsc --noEmit',
44
55
  'db:generate': 'drizzle-kit generate',
45
- 'db:migrate': 'pnpm tsx --env-file=.env src/db/migrate.ts',
56
+ 'db:migrate': 'pnpm --filter @anhedral/db db:migrate',
46
57
  'db:studio': 'drizzle-kit studio',
47
58
  'db:push': 'drizzle-kit push',
48
59
  lint: 'eslint . --ext .js,.ts',
@@ -51,14 +62,16 @@ function writePackageJson(dir, projectName) {
51
62
  },
52
63
  keywords: [],
53
64
  license: 'MIT',
65
+ dependencies: BACKEND_DEPENDENCIES.dependencies,
66
+ devDependencies: BACKEND_DEPENDENCIES.devDependencies,
54
67
  }, null, 2) + '\n');
55
68
  }
56
69
  function writeTsConfig(dir) {
57
70
  writeFile(path.join(dir, 'tsconfig.json'), JSON.stringify({
58
71
  compilerOptions: {
59
72
  target: 'ESNext',
60
- module: 'NodeNext',
61
- moduleResolution: 'NodeNext',
73
+ module: 'ESNext',
74
+ moduleResolution: 'Bundler',
62
75
  skipLibCheck: true,
63
76
  rootDir: '.',
64
77
  outDir: 'dist',
@@ -104,6 +117,7 @@ function writeEnvExample(dir, frontendUrl = 'http://localhost:8081') {
104
117
  PORT=8787
105
118
  NODE_ENV=development
106
119
  LOG_LEVEL=info
120
+ ANHEDRAL_DEMO=false
107
121
 
108
122
  # Database (NeonDB)
109
123
  DATABASE_URL="postgresql://neondb_owner:***@***.neon.tech/neondb?sslmode=require"
@@ -134,6 +148,7 @@ function writeEnvFile(dir, frontendUrl = 'http://localhost:8081') {
134
148
  PORT=8787
135
149
  NODE_ENV=development
136
150
  LOG_LEVEL=info
151
+ ANHEDRAL_DEMO=true
137
152
 
138
153
  # Database (NeonDB)
139
154
  DATABASE_URL="postgresql://user:pass@localhost:5432/app?sslmode=disable"
@@ -162,8 +177,8 @@ RC_OFFERING_ID=default
162
177
  function writeDrizzleConfig(dir) {
163
178
  writeFile(path.join(dir, 'drizzle.config.ts'), `import type { Config } from 'drizzle-kit';
164
179
  export default {
165
- schema: './src/db/schema.ts',
166
- out: './src/db/migrations',
180
+ schema: '../../packages/db/src/schema.ts',
181
+ out: '../../packages/db/migrations',
167
182
  dialect: 'postgresql',
168
183
  dbCredentials: { url: process.env.DATABASE_URL! }
169
184
  } satisfies Config;
@@ -184,13 +199,6 @@ export default defineConfig({
184
199
  });
185
200
  `);
186
201
  }
187
- function writeVercelJson(dir) {
188
- writeFile(path.join(dir, 'vercel.json'), JSON.stringify({
189
- version: 2,
190
- builds: [{ src: 'api/index.ts', use: '@vercel/node' }],
191
- routes: [{ src: '/(.*)', dest: 'api/index.ts' }],
192
- }, null, 2) + '\n');
193
- }
194
202
  // ═══════════════════════════════════════════════════════════════════════════
195
203
  // Types
196
204
  // ═══════════════════════════════════════════════════════════════════════════
@@ -207,6 +215,7 @@ export interface AppEnv {
207
215
  PORT: number;
208
216
  NODE_ENV: string;
209
217
  LOG_LEVEL: string;
218
+ ANHEDRAL_DEMO?: string | null;
210
219
  CLERK_PUBLISHABLE_KEY?: string | null;
211
220
  CLERK_SECRET_KEY?: string | null;
212
221
  FRONTEND_URL?: string | null;
@@ -265,183 +274,12 @@ export {};
265
274
  // Database
266
275
  // ═══════════════════════════════════════════════════════════════════════════
267
276
  function writeDbFiles(dir) {
268
- writeFile(path.join(dir, 'src/db/index.ts'), `import { drizzle } from 'drizzle-orm/neon-http';
269
- import { neon } from '@neondatabase/serverless';
270
-
271
- if (!process.env.DATABASE_URL) {
272
- throw new Error('DATABASE_URL is not defined');
273
- }
274
-
275
- const sql = neon(process.env.DATABASE_URL);
276
- export const db = drizzle(sql);
277
- export type Database = typeof db;
277
+ writeFile(path.join(dir, 'src/db/index.ts'), `export { db } from '@anhedral/db';
278
+ export type { Database } from '@anhedral/db';
278
279
  `);
279
- writeFile(path.join(dir, 'src/db/schema.ts'), `import { pgTable, text, timestamp, boolean, integer, index, jsonb } from 'drizzle-orm/pg-core';
280
- import type { InferSelectModel, InferInsertModel } from 'drizzle-orm';
281
-
282
- export const SUBSCRIPTION_TIERS = ['free', 'pro'] as const;
283
- export type SubscriptionTier = (typeof SUBSCRIPTION_TIERS)[number];
284
-
285
- export const SUBSCRIPTION_STATUSES = ['active', 'expired', 'canceled'] as const;
286
- export type SubscriptionStatus = (typeof SUBSCRIPTION_STATUSES)[number];
287
-
288
- export const SUBSCRIPTION_METHODS = ['trialing', 'redeemed', 'paid'] as const;
289
- export type SubscriptionMethod = (typeof SUBSCRIPTION_METHODS)[number];
290
-
291
- export const SUBSCRIPTION_ORIGINS = ['web', 'apple', 'google'] as const;
292
- export type SubscriptionOrigin = (typeof SUBSCRIPTION_ORIGINS)[number];
293
-
294
- export const SUBSCRIPTION_EVENT_TYPES = [
295
- 'trial_started', 'trial_converted', 'trial_expired',
296
- 'initial_purchase', 'renewal', 'product_change',
297
- 'cancellation_scheduled', 'cancellation_unscheduled', 'subscription_expired', 'subscription_canceled',
298
- 'promo_redeemed', 'billing_issue', 'billing_recovered',
299
- ] as const;
300
- export type SubscriptionEventType = (typeof SUBSCRIPTION_EVENT_TYPES)[number];
301
-
302
- export type SubscriptionMetadata = {
303
- revenueCatProductId?: string; lastWebhookUpdate?: string; cancelReason?: string;
304
- redeemCode?: string; redeemCodeRedeemedAt?: string;
305
- };
306
-
307
- export type SubscriptionEventMetadata = {
308
- revenueCatEventType?: string; revenueCatProductId?: string; promoCode?: string;
309
- billingPeriod?: string; price?: { amount: number; currency: string };
310
- store?: string; transactionId?: string; reason?: string;
311
- };
312
-
313
- export const PROMO_CODE_DURATIONS = [1, 6, 12] as const;
314
- export type PromoCodeDuration = (typeof PROMO_CODE_DURATIONS)[number];
315
-
316
- export const users = pgTable('users', {
317
- id: text('id').primaryKey(),
318
- email: text('email').notNull().unique(),
319
- displayName: text('display_name'),
320
- lastLoginAt: timestamp('last_login_at'),
321
- profileImageUrl: text('profile_image_url'),
322
- avatarObjectKey: text('avatar_object_key'),
323
- avatarMimeType: text('avatar_mime_type'),
324
- creditsBalance: integer('credits_balance').notNull().default(250),
325
- createdAt: timestamp('created_at').$defaultFn(() => new Date()).notNull(),
326
- });
327
-
328
- export const subscriptions = pgTable('subscriptions', {
329
- id: text('id').primaryKey(),
330
- userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }).unique(),
331
- tier: text('tier').$type<SubscriptionTier>().notNull().default('free'),
332
- status: text('status').$type<SubscriptionStatus>().notNull().default('active'),
333
- method: text('method').$type<SubscriptionMethod>(),
334
- origin: text('origin').$type<SubscriptionOrigin>(),
335
- billingPeriod: text('billing_period'),
336
- currentPeriodStart: timestamp('current_period_start'),
337
- currentPeriodEnd: timestamp('current_period_end'),
338
- canceledAt: timestamp('canceled_at'),
339
- cancelAtPeriodEnd: boolean('cancel_at_period_end').notNull().default(false),
340
- trialStart: timestamp('trial_start'),
341
- trialEnd: timestamp('trial_end'),
342
- dailyLimit: integer('daily_limit'),
343
- metadata: jsonb('metadata').$type<SubscriptionMetadata>(),
344
- createdAt: timestamp('created_at').$defaultFn(() => new Date()).notNull(),
345
- updatedAt: timestamp('updated_at').$defaultFn(() => new Date()).notNull(),
346
- }, (t) => [
347
- index('subscriptions_user_idx').on(t.userId),
348
- index('subscriptions_status_idx').on(t.status),
349
- index('subscriptions_tier_idx').on(t.tier),
350
- index('subscriptions_period_end_idx').on(t.currentPeriodEnd),
351
- ]);
352
-
353
- export const subscriptionEvents = pgTable('subscription_events', {
354
- id: text('id').primaryKey(),
355
- userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
356
- subscriptionId: text('subscription_id').references(() => subscriptions.id, { onDelete: 'set null' }),
357
- eventType: text('event_type').$type<SubscriptionEventType>().notNull(),
358
- previousTier: text('previous_tier').$type<SubscriptionTier>(),
359
- previousStatus: text('previous_status').$type<SubscriptionStatus>(),
360
- previousMethod: text('previous_method').$type<SubscriptionMethod>(),
361
- newTier: text('new_tier').$type<SubscriptionTier>(),
362
- newStatus: text('new_status').$type<SubscriptionStatus>(),
363
- newMethod: text('new_method').$type<SubscriptionMethod>(),
364
- revenueCatEventType: text('revenuecat_event_type'),
365
- revenueCatProductId: text('revenuecat_product_id'),
366
- origin: text('origin').$type<SubscriptionOrigin>(),
367
- periodStart: timestamp('period_start'),
368
- periodEnd: timestamp('period_end'),
369
- metadata: jsonb('metadata').$type<SubscriptionEventMetadata>(),
370
- createdAt: timestamp('created_at').$defaultFn(() => new Date()).notNull(),
371
- }, (t) => [
372
- index('sub_events_user_idx').on(t.userId),
373
- index('sub_events_user_created_idx').on(t.userId, t.createdAt),
374
- index('sub_events_type_idx').on(t.eventType),
375
- ]);
376
-
377
- export const trialClaims = pgTable('trial_claims', {
378
- id: text('id').primaryKey(),
379
- userId: text('user_id').references(() => users.id, { onDelete: 'set null' }),
380
- email: text('email').notNull().unique(),
381
- claimedAt: timestamp('claimed_at').$defaultFn(() => new Date()).notNull(),
382
- }, (t) => [index('trial_claims_email_idx').on(t.email)]);
383
-
384
- export const promoCodes = pgTable('promo_codes', {
385
- id: text('id').primaryKey(),
386
- code: text('code').notNull().unique(),
387
- months: integer('months').$type<PromoCodeDuration>().notNull(),
388
- maxRedemptions: integer('max_redemptions').notNull().default(1),
389
- redeemedCount: integer('redeemed_count').notNull().default(0),
390
- expiresAt: timestamp('expires_at'),
391
- createdAt: timestamp('created_at').$defaultFn(() => new Date()).notNull(),
392
- }, (t) => [index('promo_codes_code_idx').on(t.code)]);
393
-
394
- export const promoRedemptions = pgTable('promo_redemptions', {
395
- id: text('id').primaryKey(),
396
- promoCodeId: text('promo_code_id').notNull().references(() => promoCodes.id, { onDelete: 'cascade' }),
397
- userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
398
- redeemedAt: timestamp('redeemed_at').$defaultFn(() => new Date()).notNull(),
399
- entitlementExpiresAt: timestamp('entitlement_expires_at').notNull(),
400
- }, (t) => [
401
- index('promo_redemptions_user_idx').on(t.userId),
402
- index('promo_redemptions_code_idx').on(t.promoCodeId),
403
- ]);
404
-
405
- export type Users = InferSelectModel<typeof users>;
406
- export type NewUsers = InferInsertModel<typeof users>;
407
- export type Subscriptions = InferSelectModel<typeof subscriptions>;
408
- export type NewSubscriptions = InferInsertModel<typeof subscriptions>;
409
- export type SubscriptionEvents = InferSelectModel<typeof subscriptionEvents>;
410
- export type NewSubscriptionEvents = InferInsertModel<typeof subscriptionEvents>;
411
- export type TrialClaims = InferSelectModel<typeof trialClaims>;
412
- export type NewTrialClaims = InferInsertModel<typeof trialClaims>;
413
- export type PromoCodes = InferSelectModel<typeof promoCodes>;
414
- export type NewPromoCodes = InferInsertModel<typeof promoCodes>;
415
- export type PromoRedemptions = InferSelectModel<typeof promoRedemptions>;
416
- export type NewPromoRedemptions = InferInsertModel<typeof promoRedemptions>;
280
+ writeFile(path.join(dir, 'src/db/schema.ts'), `export * from '@anhedral/db/schema';
417
281
  `);
418
- writeFile(path.join(dir, 'src/db/migrate.ts'), `import { drizzle } from 'drizzle-orm/neon-http';
419
- import { migrate } from 'drizzle-orm/neon-http/migrator';
420
- import { neon } from '@neondatabase/serverless';
421
- import dotenv from 'dotenv';
422
-
423
- dotenv.config({ quiet: true });
424
-
425
- const runMigrate = async () => {
426
- if (!process.env.DATABASE_URL) {
427
- throw new Error('DATABASE_URL is not defined');
428
- }
429
-
430
- const client = neon(process.env.DATABASE_URL);
431
- const db = drizzle(client);
432
-
433
- console.log('⏳ Running migrations...');
434
- const start = Date.now();
435
- await migrate(db, { migrationsFolder: './src/db/migrations' });
436
- console.log('✅ Migrations completed in', Date.now() - start, 'ms');
437
- process.exit(0);
438
- };
439
-
440
- runMigrate().catch((err) => {
441
- console.error('❌ Migration failed');
442
- console.error(err);
443
- process.exit(1);
444
- });
282
+ writeFile(path.join(dir, 'src/db/migrate.ts'), `import '@anhedral/db/migrate';
445
283
  `);
446
284
  }
447
285
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1297,6 +1135,7 @@ const configPlugin: FastifyPluginAsync = async (fastify: FastifyInstance) => {
1297
1135
  PORT: { type: 'number' },
1298
1136
  NODE_ENV: { type: 'string', default: 'development' },
1299
1137
  LOG_LEVEL: { type: 'string', default: 'info' },
1138
+ ANHEDRAL_DEMO: { type: 'string', default: 'false' },
1300
1139
  CLERK_PUBLISHABLE_KEY: { type: 'string', nullable: true },
1301
1140
  CLERK_SECRET_KEY: { type: 'string', nullable: true },
1302
1141
  FRONTEND_URL: { type: 'string', nullable: true },
@@ -1306,8 +1145,8 @@ const configPlugin: FastifyPluginAsync = async (fastify: FastifyInstance) => {
1306
1145
  R2_ACCESS_KEY_ID: { type: 'string', nullable: true },
1307
1146
  R2_SECRET_ACCESS_KEY: { type: 'string', nullable: true },
1308
1147
  R2_BUCKET: { type: 'string', nullable: true },
1309
- RC_SECRET_API_KEY: { type: 'string', default: '' },
1310
- RC_WEBHOOK_SECRET: { type: 'string', default: '' },
1148
+ RC_SECRET_API_KEY: { type: 'string', nullable: true, default: '' },
1149
+ RC_WEBHOOK_SECRET: { type: 'string', nullable: true, default: '' },
1311
1150
  RC_ENTITLEMENT_ID: { type: 'string', default: 'pro' },
1312
1151
  RC_OFFERING_ID: { type: 'string', default: 'default' },
1313
1152
  },
@@ -1318,6 +1157,23 @@ const configPlugin: FastifyPluginAsync = async (fastify: FastifyInstance) => {
1318
1157
  dotenv: !process.env.VERCEL,
1319
1158
  confKey: 'env',
1320
1159
  } as unknown as Record<string, unknown>);
1160
+
1161
+ if (fastify.env.NODE_ENV === 'production') {
1162
+ if (fastify.env.ANHEDRAL_DEMO === 'true') {
1163
+ throw new Error('ANHEDRAL_DEMO must be false in production');
1164
+ }
1165
+
1166
+ const required = [
1167
+ 'CLERK_PUBLISHABLE_KEY',
1168
+ 'CLERK_SECRET_KEY',
1169
+ 'RC_SECRET_API_KEY',
1170
+ 'RC_WEBHOOK_SECRET',
1171
+ ] as const;
1172
+ const missing = required.filter((key) => !fastify.env[key]);
1173
+ if (missing.length > 0) {
1174
+ throw new Error(\`Missing production environment variables: \${missing.join(', ')}\`);
1175
+ }
1176
+ }
1321
1177
  };
1322
1178
 
1323
1179
  export default fp(configPlugin, { name: 'env-config', fastify: '5.x' });
@@ -1366,6 +1222,15 @@ export const clerkAuthPlugin: FastifyPluginAsync = async (fastify: FastifyInstan
1366
1222
  const authenticate = async (req: FastifyRequest, _reply: FastifyReply) => {
1367
1223
  if (req.method === 'OPTIONS' || req.url.startsWith('/health')) return;
1368
1224
 
1225
+ if (fastify.env?.ANHEDRAL_DEMO === 'true') {
1226
+ req.user = {
1227
+ id: 'user_demo',
1228
+ subscriptionTier: 'pro',
1229
+ subscriptionStatus: 'active',
1230
+ };
1231
+ return;
1232
+ }
1233
+
1369
1234
  const authHeader = req.headers.authorization;
1370
1235
  if (!authHeader?.startsWith('Bearer ')) throw AuthError.unauthorized();
1371
1236
 
@@ -1929,6 +1794,23 @@ const authRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => {
1929
1794
  }, async (req: FastifyRequest, reply: FastifyReply) => {
1930
1795
  if (!req.user?.id) throw AuthError.unauthorized();
1931
1796
 
1797
+ if (fastify.env.ANHEDRAL_DEMO === 'true') {
1798
+ return reply.send({
1799
+ user: {
1800
+ id: 'user_demo',
1801
+ email: 'demo@anhedral.dev',
1802
+ firstName: 'Demo',
1803
+ lastName: 'Builder',
1804
+ displayName: '${displayName} Demo',
1805
+ imageUrl: null,
1806
+ avatarUrl: null,
1807
+ creditsBalance: 250,
1808
+ subscriptionTier: 'pro',
1809
+ subscriptionStatus: 'active',
1810
+ },
1811
+ });
1812
+ }
1813
+
1932
1814
  const clerkUser = await clerkClient.users.getUser(req.user.id);
1933
1815
  const userData = await fastify.repos.users.getDashboardProfile(req.user.id);
1934
1816
  const primaryEmail = clerkUser.emailAddresses.find(
@@ -2048,6 +1930,19 @@ const subscriptionRoutes: FastifyPluginAsync = async (fastify: FastifyInstance)
2048
1930
  }, async (req, reply) => {
2049
1931
  const userId = req.user?.id;
2050
1932
  if (!userId) throw AuthError.unauthorized();
1933
+ if (app.env.ANHEDRAL_DEMO === 'true') {
1934
+ reply.header('Cache-Control', 'private, no-store');
1935
+ return reply.send({
1936
+ pro: true,
1937
+ inTrial: false,
1938
+ expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
1939
+ periodStart: new Date().toISOString(),
1940
+ periodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
1941
+ method: 'paid',
1942
+ managementUrl: 'https://app.revenuecat.com/',
1943
+ cancelAtPeriodEnd: false,
1944
+ });
1945
+ }
2051
1946
  const refreshRaw = (req.query as unknown as { refresh?: unknown }).refresh;
2052
1947
  const requestedRefresh = refreshRaw === true || refreshRaw === 'true' || refreshRaw === 1 || refreshRaw === '1';
2053
1948
  const forceRefresh = Boolean(app.env.RC_SECRET_API_KEY) && requestedRefresh;
@@ -2061,6 +1956,9 @@ const subscriptionRoutes: FastifyPluginAsync = async (fastify: FastifyInstance)
2061
1956
  }, async (req, reply) => {
2062
1957
  const userId = req.user?.id;
2063
1958
  if (!userId) throw AuthError.unauthorized();
1959
+ if (app.env.ANHEDRAL_DEMO === 'true') {
1960
+ return { ok: true, expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() };
1961
+ }
2064
1962
  const { code } = req.body;
2065
1963
  const validation = await app.repos.promoCodes.validateCode(code, userId);
2066
1964
  if (!validation.valid || !validation.promoCode) {
@@ -2299,10 +2197,34 @@ async function getApp(): Promise<FastifyInstance> {
2299
2197
 
2300
2198
  export default async function handler(req: VercelRequest, res: VercelResponse) {
2301
2199
  const app = await getApp();
2200
+ if (req.url === '/api' || req.url === '/backend') {
2201
+ req.url = '/';
2202
+ } else if (req.url?.startsWith('/api/')) {
2203
+ req.url = req.url.slice('/api'.length);
2204
+ } else if (req.url?.startsWith('/backend/')) {
2205
+ req.url = req.url.slice('/backend'.length);
2206
+ }
2302
2207
  app.server.emit('request', req, res);
2303
2208
  }
2304
2209
  `);
2305
2210
  }
2211
+ function writeVercelConfig(dir) {
2212
+ writeFile(path.join(dir, 'vercel.json'), JSON.stringify({
2213
+ version: 2,
2214
+ builds: [
2215
+ {
2216
+ src: 'api/index.ts',
2217
+ use: '@vercel/node',
2218
+ },
2219
+ ],
2220
+ routes: [
2221
+ {
2222
+ src: '/(.*)',
2223
+ dest: 'api/index.ts',
2224
+ },
2225
+ ],
2226
+ }, null, 2) + '\n');
2227
+ }
2306
2228
  // ═══════════════════════════════════════════════════════════════════════════
2307
2229
  // Tests
2308
2230
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1,2 +1,2 @@
1
1
  import type { ProjectOptions } from '../scaffold.js';
2
- export declare function scaffoldExtension(root: string, { projectName, displayName }: ProjectOptions): Promise<void>;
2
+ export declare function scaffoldExtension(root: string, { projectName, displayName, skipInstall }: ProjectOptions): Promise<void>;