spfn 0.2.0-beta.9 → 0.3.0-beta.2

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.
Files changed (41) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +626 -204
  3. package/bin/spfn.js +53 -4
  4. package/dist/index.js +4183 -2281
  5. package/dist/templates/.dockerignore +1 -0
  6. package/dist/templates/Dockerfile +1 -1
  7. package/dist/templates/README.md +94 -0
  8. package/dist/templates/lib/api-client.ts +1 -1
  9. package/dist/templates/modes/full/src/app/auth/callback/page.tsx +1 -0
  10. package/dist/templates/modes/full/src/app/login/page.tsx +68 -0
  11. package/dist/templates/modes/full/src/i18n/catalogs.ts +16 -0
  12. package/dist/templates/modes/full/src/i18n/server.ts +9 -0
  13. package/dist/templates/modes/full/src/server/config/env.config.ts +23 -0
  14. package/dist/templates/modes/full/src/server/mcp.ts +75 -0
  15. package/dist/templates/modes/full/src/server/router.ts +32 -0
  16. package/dist/templates/modes/full/src/server/routes/examples.ts +94 -0
  17. package/dist/templates/modes/full/src/server/routes/health.ts +9 -0
  18. package/dist/templates/modes/full/src/server/routes/root.ts +15 -0
  19. package/dist/templates/modes/full/src/server/server.config.ts +14 -0
  20. package/dist/templates/server/config/env.config.ts +10 -75
  21. package/dist/templates/server/entities/config.ts +1 -1
  22. package/dist/templates/server/entities/example.entity.ts +1 -1
  23. package/dist/templates/server/repositories/example.repository.ts +1 -1
  24. package/dist/templates/server/router.ts +2 -3
  25. package/dist/templates/server/routes/examples.ts +2 -12
  26. package/dist/templates/server/routes/health.ts +1 -1
  27. package/dist/templates/server/routes/root.ts +1 -2
  28. package/dist/templates/server/server.config.ts +2 -2
  29. package/dist/templates/vercel/npmrc +4 -0
  30. package/dist/templates/vercel/route.ts +53 -0
  31. package/dist/templates/vercel/vercel.json +6 -0
  32. package/package.json +35 -23
  33. package/dist/commands/generate/templates/contract.template +0 -58
  34. package/dist/commands/generate/templates/entity.template +0 -28
  35. package/dist/commands/generate/templates/init-migration.template +0 -3
  36. package/dist/commands/generate/templates/repository.template +0 -56
  37. package/dist/commands/generate/templates/route.template +0 -65
  38. package/dist/commands/generate/templates/schema.template +0 -13
  39. package/dist/templates/Dockerfile.optimized +0 -67
  40. package/dist/templates/config/index.ts +0 -22
  41. package/dist/templates/config/schema.ts +0 -42
@@ -2,10 +2,10 @@
2
2
  * Server Configuration
3
3
  */
4
4
  import { defineServerConfig } from '@spfn/core/server';
5
- import { appRouter } from "@/server/router";
5
+ import { appRouter } from '@/server/router';
6
6
 
7
7
  export default defineServerConfig()
8
8
  .port(8790)
9
9
  .host('0.0.0.0')
10
10
  .routes(appRouter)
11
- .build();
11
+ .build();
@@ -0,0 +1,4 @@
1
+ @spfn:registry=https://git.superfunction.xyz/api/packages/superfunction/npm/
2
+ @superself:registry=https://git.superfunction.xyz/api/packages/superfunction/npm/
3
+ //git.superfunction.xyz/api/packages/superfunction/npm/:_authToken=${GITEA_NPM_TOKEN}
4
+ //git.superfunction.xyz/api/packages/superfunction/npm/:always-auth=true
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Vercel serverless adapter for the SPFN Hono API.
3
+ *
4
+ * The SPFN-specific bootstrap lives in the core: `createServerlessApp()`
5
+ * (from `@spfn/core/server`) returns a listen-free, memoized Hono app with the DB
6
+ * initialized in-handler and the periodic health-check disabled. `hono/vercel`'s
7
+ * `handle()` turns it into Vercel Functions. `startServer()` / `spfn start` (the
8
+ * always-on path) is unaffected.
9
+ *
10
+ * The SPFN app is mounted under `/api/backend` so it coexists with Next.js routes on
11
+ * a single Vercel origin. Point the RPC proxy at the same origin in production:
12
+ * SPFN_API_URL = https://<deployment>/api/backend
13
+ */
14
+
15
+ import { Hono } from 'hono';
16
+ import { handle } from 'hono/vercel';
17
+ import { createServerlessApp } from '@spfn/core/server';
18
+ import serverConfig from '@/server/server.config';
19
+
20
+ // SPFN needs Node APIs (pg, native bcrypt) — the edge runtime cannot host it.
21
+ export const runtime = 'nodejs';
22
+ export const dynamic = 'force-dynamic';
23
+
24
+ // The Vercel Supabase integration injects POSTGRES_URL (Supavisor transaction pooler)
25
+ // but not DATABASE_URL, which is what SPFN reads. Map it so the app connects.
26
+ process.env.DATABASE_URL ??= process.env.POSTGRES_URL;
27
+
28
+ let handlerPromise: Promise<(req: Request) => Response | Promise<Response>> | undefined;
29
+
30
+ function getHandler()
31
+ {
32
+ if (!handlerPromise)
33
+ {
34
+ handlerPromise = createServerlessApp(serverConfig).then((spfn) =>
35
+ handle(new Hono().route('/api/backend', spfn)),
36
+ );
37
+ }
38
+
39
+ return handlerPromise;
40
+ }
41
+
42
+ async function dispatch(req: Request): Promise<Response>
43
+ {
44
+ return (await getHandler())(req);
45
+ }
46
+
47
+ export const GET = dispatch;
48
+ export const POST = dispatch;
49
+ export const PUT = dispatch;
50
+ export const PATCH = dispatch;
51
+ export const DELETE = dispatch;
52
+ export const OPTIONS = dispatch;
53
+ export const HEAD = dispatch;
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://openapi.vercel.sh/vercel.json",
3
+ "framework": "nextjs",
4
+ "buildCommand": "pnpm spfn:build",
5
+ "installCommand": "pnpm install --no-frozen-lockfile"
6
+ }
package/package.json CHANGED
@@ -1,16 +1,26 @@
1
1
  {
2
2
  "name": "spfn",
3
- "version": "0.2.0-beta.9",
4
- "description": "Superfunction CLI - Add SPFN to your Next.js project",
3
+ "version": "0.3.0-beta.2",
4
+ "description": "Scaffold a full-stack TypeScript backend onto a Next.js app built with an AI coding agent: auth, database, typed routes and codegen, one fixed vertical slice per feature",
5
5
  "type": "module",
6
6
  "bin": {
7
- "spfn": "./bin/spfn.js"
7
+ "spfn": "bin/spfn.js"
8
8
  },
9
9
  "types": "./dist/index.d.ts",
10
10
  "files": [
11
11
  "dist",
12
12
  "bin"
13
13
  ],
14
+ "scripts": {
15
+ "build": "tsup && node scripts/copy-templates.js",
16
+ "test": "vitest run",
17
+ "prepack": "node -e \"require('fs').copyFileSync('../../LICENSE','LICENSE')\"",
18
+ "dev": "tsup --watch",
19
+ "type-check": "tsc --noEmit",
20
+ "publish:alpha": "node ../../scripts/publish-package.mjs alpha",
21
+ "publish:beta": "node ../../scripts/publish-package.mjs beta",
22
+ "publish:latest": "node ../../scripts/publish-package.mjs latest"
23
+ },
14
24
  "keywords": [
15
25
  "spfn",
16
26
  "cli",
@@ -22,31 +32,32 @@
22
32
  "scaffolding",
23
33
  "boilerplate",
24
34
  "dev-tools",
25
- "development",
26
- "init",
27
35
  "typescript",
28
- "type-safe"
36
+ "type-safe",
37
+ "full-stack-framework",
38
+ "ai-coding-agent",
39
+ "claude-code"
29
40
  ],
30
- "author": "Ray Im <rayim@inflike.com>",
41
+ "author": "Ray Im <rayim@fxy.global>",
31
42
  "license": "MIT",
32
43
  "repository": {
33
44
  "type": "git",
34
- "url": "https://github.com/spfn/spfn.git",
45
+ "url": "git+https://github.com/fxylabs/spfn.git",
35
46
  "directory": "packages/cli"
36
47
  },
37
48
  "publishConfig": {
38
49
  "access": "public",
39
- "tag": "alpha",
40
- "registry": "https://registry.npmjs.org"
50
+ "tag": "beta"
41
51
  },
42
52
  "bugs": {
43
- "url": "https://github.com/spfn/spfn/issues"
53
+ "url": "https://github.com/fxylabs/spfn/issues"
44
54
  },
45
- "homepage": "https://github.com/spfn/spfn/tree/main/packages/cli#readme",
55
+ "homepage": "https://github.com/fxylabs/spfn/tree/main/packages/cli#readme",
46
56
  "engines": {
47
- "node": ">=18.18.0"
57
+ "node": ">=20.0.0"
48
58
  },
49
59
  "peerDependencies": {
60
+ "@spfn/core": ">=0.3.0-beta.1 <0.4.0",
50
61
  "typescript": "^5.3.0"
51
62
  },
52
63
  "peerDependenciesMeta": {
@@ -59,7 +70,8 @@
59
70
  "chokidar": "^4.0.3",
60
71
  "commander": "^11.1.0",
61
72
  "dotenv": "^17.2.3",
62
- "drizzle-orm": "^0.45.0",
73
+ "drizzle-kit": "1.0.0-rc.4",
74
+ "drizzle-orm": "1.0.0-rc.4",
63
75
  "execa": "^8.0.1",
64
76
  "fs-extra": "^11.2.0",
65
77
  "ora": "^7.0.1",
@@ -67,21 +79,21 @@
67
79
  "postgres": "^3.4.0",
68
80
  "prompts": "^2.4.2",
69
81
  "tsup": "^8.5.0",
70
- "@spfn/core": "0.2.0-beta.12"
82
+ "yaml": "^2.8.3"
83
+ },
84
+ "optionalDependencies": {
85
+ "@napi-rs/keyring": "^1.1.0"
71
86
  },
72
87
  "devDependencies": {
88
+ "@electric-sql/pglite": "^0.5.4",
89
+ "@spfn/core": "workspace:*",
73
90
  "@types/fs-extra": "^11.0.4",
74
91
  "@types/node": "^20.11.0",
75
92
  "@types/pg": "^8.10.9",
76
93
  "@types/prompts": "^2.4.9",
77
94
  "concurrently": "^9.2.1",
78
- "drizzle-kit": "^0.31.6",
79
95
  "tsx": "^4.20.6",
80
- "typescript": "^5.3.3"
81
- },
82
- "scripts": {
83
- "build": "tsup && node scripts/copy-templates.js",
84
- "dev": "tsup --watch",
85
- "type-check": "tsc --noEmit"
96
+ "typescript": "^5.3.3",
97
+ "vitest": "^4.0.6"
86
98
  }
87
- }
99
+ }
@@ -1,58 +0,0 @@
1
- /**
2
- * {{PASCAL_NAME}} Contracts
3
- *
4
- * Auto-generated by SPFN CLI
5
- */
6
-
7
- import { Type } from '@sinclair/typebox';
8
- import { createContract } from '@spfn/core/route';
9
-
10
- /**
11
- * GET /{{ENTITY_NAME}}
12
- * List {{PASCAL_NAME}} items
13
- */
14
- export const list{{PASCAL_NAME}}Contract = createContract({
15
- method: 'GET',
16
- path: '/{{ENTITY_NAME}}',
17
- query: Type.Object({
18
- limit: Type.Optional(Type.Number({ minimum: 1, maximum: 100 })),
19
- offset: Type.Optional(Type.Number({ minimum: 0 })),
20
- }),
21
- responses: {
22
- 200: Type.Object({
23
- items: Type.Array(
24
- Type.Object({
25
- id: Type.Number(),
26
- name: Type.String(),
27
- description: Type.Union([Type.String(), Type.Null()]),
28
- createdAt: Type.String(),
29
- updatedAt: Type.String(),
30
- })
31
- ),
32
- limit: Type.Number(),
33
- offset: Type.Number(),
34
- }),
35
- },
36
- });
37
-
38
- /**
39
- * POST /{{ENTITY_NAME}}
40
- * Create new {{PASCAL_NAME}}
41
- */
42
- export const create{{PASCAL_NAME}}Contract = createContract({
43
- method: 'POST',
44
- path: '/{{ENTITY_NAME}}',
45
- body: Type.Object({
46
- name: Type.String({ minLength: 1 }),
47
- description: Type.Optional(Type.String()),
48
- }),
49
- responses: {
50
- 201: Type.Object({
51
- id: Type.Number(),
52
- name: Type.String(),
53
- description: Type.Union([Type.String(), Type.Null()]),
54
- createdAt: Type.String(),
55
- updatedAt: Type.String(),
56
- }),
57
- },
58
- });
@@ -1,28 +0,0 @@
1
- /**
2
- * {{PASCAL_NAME}} Entity
3
- *
4
- * Auto-generated by SPFN CLI
5
- */
6
-
7
- import { index, serial, text, timestamp } from 'drizzle-orm/pg-core';
8
- import { {{SCHEMA_VAR_NAME}} } from './{{SCHEMA_FILE_NAME}}';
9
-
10
- export const {{ENTITY_NAME}} = {{SCHEMA_VAR_NAME}}.table('{{TABLE_NAME}}', {
11
- // Primary Key
12
- id: serial('id').primaryKey(),
13
-
14
- // Fields
15
- name: text('name').notNull(),
16
- description: text('description'),
17
-
18
- // Timestamps
19
- createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
20
- updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
21
- }, (table) => [
22
- // Indexes
23
- index('{{SCHEMA_NAME}}_{{TABLE_NAME}}_name_idx').on(table.name),
24
- ]);
25
-
26
- // Type inference
27
- export type {{PASCAL_NAME}} = typeof {{ENTITY_NAME}}.$inferSelect;
28
- export type New{{PASCAL_NAME}} = typeof {{ENTITY_NAME}}.$inferInsert;
@@ -1,3 +0,0 @@
1
- -- Initial migration for @spfn/{{FN_NAME}}
2
- -- Create schema for {{PASCAL_NAME}} package
3
- CREATE SCHEMA IF NOT EXISTS spfn_{{FN_NAME}};
@@ -1,56 +0,0 @@
1
- /**
2
- * {{PASCAL_NAME}} Repository
3
- *
4
- * Auto-generated by SPFN CLI
5
- */
6
-
7
- import { findOne, findMany, create, update, deleteById } from '@spfn/core/db';
8
- import { eq } from 'drizzle-orm';
9
- import { {{ENTITY_NAME}} } from '@/entities';
10
- import type { {{PASCAL_NAME}}, New{{PASCAL_NAME}} } from '@/entities';
11
-
12
- export const {{REPO_NAME}} = {
13
- /**
14
- * Find {{PASCAL_NAME}} by ID
15
- */
16
- findById: async (id: number): Promise<{{PASCAL_NAME}} | null> =>
17
- {
18
- return findOne({{ENTITY_NAME}}, eq({{ENTITY_NAME}}.id, id));
19
- },
20
-
21
- /**
22
- * Find all {{PASCAL_NAME}} items
23
- */
24
- findMany: async (options?: { limit?: number; offset?: number }): Promise<{{PASCAL_NAME}}[]> =>
25
- {
26
- const { limit = 20, offset = 0 } = options || {};
27
- return findMany({{ENTITY_NAME}}, {
28
- limit,
29
- offset,
30
- });
31
- },
32
-
33
- /**
34
- * Create new {{PASCAL_NAME}}
35
- */
36
- create: async (data: New{{PASCAL_NAME}}): Promise<{{PASCAL_NAME}}> =>
37
- {
38
- return create({{ENTITY_NAME}}, data);
39
- },
40
-
41
- /**
42
- * Update {{PASCAL_NAME}} by ID
43
- */
44
- updateById: async (id: number, data: Partial<New{{PASCAL_NAME}}>): Promise<{{PASCAL_NAME}} | null> =>
45
- {
46
- return update({{ENTITY_NAME}}, eq({{ENTITY_NAME}}.id, id), data);
47
- },
48
-
49
- /**
50
- * Delete {{PASCAL_NAME}} by ID
51
- */
52
- deleteById: async (id: number): Promise<void> =>
53
- {
54
- await deleteById({{ENTITY_NAME}}, id);
55
- },
56
- };
@@ -1,65 +0,0 @@
1
- /**
2
- * {{PASCAL_NAME}} Routes
3
- *
4
- * Auto-generated by SPFN CLI
5
- */
6
-
7
- import { createApp } from '@spfn/core/route';
8
- import { Transactional } from '@spfn/core/db';
9
- import { {{REPO_NAME}} } from '@/repositories/{{KEBAB_NAME}}.repository.js';
10
- import {
11
- list{{PASCAL_NAME}}Contract,
12
- create{{PASCAL_NAME}}Contract,
13
- } from '@/contracts/{{KEBAB_NAME}}.js';
14
-
15
- const app = createApp();
16
-
17
- /**
18
- * GET /{{ENTITY_NAME}}
19
- * List {{PASCAL_NAME}} items
20
- */
21
- app.bind(list{{PASCAL_NAME}}Contract, async (c) =>
22
- {
23
- const { limit = 20, offset = 0 } = c.query;
24
-
25
- const items = await {{REPO_NAME}}.findMany({ limit, offset });
26
-
27
- return c.json({
28
- items: items.map((item) => ({
29
- id: item.id,
30
- name: item.name,
31
- description: item.description,
32
- createdAt: item.createdAt.toISOString(),
33
- updatedAt: item.updatedAt.toISOString(),
34
- })),
35
- limit,
36
- offset,
37
- });
38
- });
39
-
40
- /**
41
- * POST /{{ENTITY_NAME}}
42
- * Create new {{PASCAL_NAME}}
43
- */
44
- app.bind(create{{PASCAL_NAME}}Contract, [Transactional()], async (c) =>
45
- {
46
- const body = await c.data();
47
-
48
- const item = await {{REPO_NAME}}.create({
49
- name: body.name,
50
- description: body.description,
51
- });
52
-
53
- return c.json(
54
- {
55
- id: item.id,
56
- name: item.name,
57
- description: item.description,
58
- createdAt: item.createdAt.toISOString(),
59
- updatedAt: item.updatedAt.toISOString(),
60
- },
61
- 201
62
- );
63
- });
64
-
65
- export default app;
@@ -1,13 +0,0 @@
1
- /**
2
- * {{FN_NAME_PASCAL}} Schema
3
- *
4
- * Creates isolated PostgreSQL schema for {{PACKAGE_NAME}} tables.
5
- * IMPORTANT: Export this schema so drizzle-kit can generate CREATE SCHEMA statement.
6
- *
7
- * Auto-generated by SPFN CLI
8
- */
9
-
10
- import { createFunctionSchema } from '@spfn/core/db';
11
-
12
- // ✅ Export schema to auto-generate CREATE SCHEMA in migrations
13
- export const {{SCHEMA_VAR_NAME}} = createFunctionSchema('{{PACKAGE_NAME}}');
@@ -1,67 +0,0 @@
1
- # Optimized Production Dockerfile for SPFN
2
- # Multi-stage build for smaller image size
3
-
4
- # Stage 1: Dependencies
5
- FROM node:20-alpine AS deps
6
- WORKDIR /app
7
-
8
- # Install pnpm
9
- RUN corepack enable pnpm
10
-
11
- # Copy dependency files
12
- COPY package.json pnpm-lock.yaml* ./
13
-
14
- # Install ALL dependencies (needed for build)
15
- RUN pnpm install --frozen-lockfile
16
-
17
- # Stage 2: Builder
18
- FROM node:20-alpine AS builder
19
- WORKDIR /app
20
-
21
- RUN corepack enable pnpm
22
-
23
- # Copy dependencies from deps stage
24
- COPY --from=deps /app/node_modules ./node_modules
25
- COPY . .
26
-
27
- # Build both Next.js and SPFN server
28
- RUN pnpm run spfn:build
29
-
30
- # Stage 3: Runner (Production)
31
- FROM node:20-alpine AS runner
32
- WORKDIR /app
33
-
34
- ENV NODE_ENV=production
35
-
36
- RUN corepack enable pnpm
37
-
38
- # Copy necessary files from builder
39
- COPY --from=builder /app/package.json ./package.json
40
- COPY --from=builder /app/pnpm-lock.yaml ./pnpm-lock.yaml
41
- COPY --from=builder /app/.next ./.next
42
- COPY --from=builder /app/.spfn ./.spfn
43
- COPY --from=builder /app/public ./public
44
- COPY --from=builder /app/next.config.* ./
45
- COPY --from=builder /app/.env.local.example ./.env.local.example
46
-
47
- # Install PRODUCTION dependencies only
48
- RUN pnpm install --frozen-lockfile --prod
49
-
50
- # Create non-root user for security
51
- RUN addgroup --system --gid 1001 nodejs
52
- RUN adduser --system --uid 1001 nextjs
53
-
54
- # Change ownership
55
- RUN chown -R nextjs:nodejs /app
56
-
57
- USER nextjs
58
-
59
- # Expose both ports
60
- EXPOSE 3790 8790
61
-
62
- # Health check for SPFN API
63
- HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
64
- CMD node -e "require('http').get('http://localhost:8790/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
65
-
66
- # Start both Next.js and SPFN server
67
- CMD ["pnpm", "run", "spfn:start"]
@@ -1,22 +0,0 @@
1
- /**
2
- * Core Package Configuration
3
- *
4
- * @example
5
- * ```typescript
6
- * import { registry } from '@spfn/core/config';
7
- *
8
- * const env = registry.validate();
9
- * console.log(env.DB_POOL_MAX);
10
- * ```
11
- *
12
- * @module config
13
- */
14
-
15
- import { createEnvRegistry } from '@spfn/core/env';
16
- import { envSchema } from './schema';
17
-
18
- /**
19
- * Environment registry
20
- */
21
- const registry = createEnvRegistry(envSchema);
22
- export const env = registry.validate();
@@ -1,42 +0,0 @@
1
- /**
2
- * Environment Variable Schema
3
- *
4
- * Centralized schema definition for all environment variables.
5
- * This provides type safety, validation, and documentation for your application configuration.
6
- *
7
- * @example
8
- * ```typescript
9
- * import { envSchema } from './config/schema';
10
- *
11
- * // Access validated environment variables
12
- * console.log(envSchema.DATABASE_URL.description);
13
- * console.log(envSchema.PORT.default);
14
- * ```
15
- *
16
- * @module config/schema
17
- */
18
-
19
- import {
20
- defineEnvSchema,
21
- envString,
22
- envNumber,
23
- } from '@spfn/core/env';
24
-
25
- /**
26
- * Application environment variable schema
27
- *
28
- * Add your environment variables here with proper types and validation.
29
- */
30
- export const envSchema = defineEnvSchema({
31
- // Example: API configuration
32
- API_KEY: envString({
33
- description: 'Third-party API key',
34
- required: true,
35
- }),
36
-
37
- // Example: Server port
38
- PORT: envNumber({
39
- description: 'Server port number',
40
- default: 8790,
41
- }),
42
- });