@spfn/core 0.1.0-alpha.2 → 0.1.0-alpha.20

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/README.md CHANGED
@@ -6,12 +6,19 @@
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue)](https://www.typescriptlang.org/)
8
8
 
9
+ > ⚠️ **Alpha Release**: SPFN is currently in alpha. APIs may change. Use `@alpha` tag for installation.
10
+
9
11
  ## Installation
10
12
 
11
- **Recommended: Use CLI**
13
+ **Recommended: Create New Project**
14
+ ```bash
15
+ npx spfn@alpha create my-app
16
+ ```
17
+
18
+ **Add to Existing Next.js Project**
12
19
  ```bash
13
- npm install -g @spfn/cli
14
- spfn init
20
+ cd your-nextjs-project
21
+ npx spfn@alpha init
15
22
  ```
16
23
 
17
24
  **Manual Installation**
@@ -478,6 +485,18 @@ Server configuration and lifecycle management.
478
485
 
479
486
  **[→ Read Server Documentation](./src/server/README.md)**
480
487
 
488
+ ### ⚙️ Code Generation
489
+ Automatic code generation with pluggable generators and centralized file watching.
490
+
491
+ **[→ Read Codegen Documentation](./src/codegen/README.md)**
492
+
493
+ **Key Features:**
494
+ - Orchestrator pattern for managing multiple generators
495
+ - Built-in contract generator for type-safe API clients
496
+ - Configuration-based setup (`.spfnrc.json` or `package.json`)
497
+ - Watch mode integrated into `spfn dev`
498
+ - Extensible with custom generators
499
+
481
500
  ## Module Exports
482
501
 
483
502
  ### Main Export
@@ -567,6 +586,7 @@ npm test -- --coverage # With coverage
567
586
  - [Error Handling](./src/errors/README.md)
568
587
  - [Middleware](./src/middleware/README.md)
569
588
  - [Server Configuration](./src/server/README.md)
589
+ - [Code Generation](./src/codegen/README.md)
570
590
 
571
591
  ### API Reference
572
592
  - See module-specific README files linked above
@@ -86,6 +86,8 @@ declare class AutoRouteLoader {
86
86
  * - users/index.ts → /users
87
87
  * - users/[id].ts → /users/:id
88
88
  * - posts/[...slug].ts → /posts/*
89
+ * - users/index.js → /users (production)
90
+ * - users/[id].js → /users/:id (production)
89
91
  */
90
92
  private fileToPath;
91
93
  /**
@@ -1,4 +1,4 @@
1
- import '../auto-loader-C44TcLmM.js';
1
+ import '../auto-loader-Pq1wY1qz.js';
2
2
  import { R as RouteContract, I as InferContract } from '../types-SlzTr8ZO.js';
3
3
  import 'hono';
4
4
  import 'hono/utils/http-status';
@@ -123,4 +123,108 @@ interface WatchGenerateOptions {
123
123
  */
124
124
  declare function watchAndGenerate(options?: WatchGenerateOptions): Promise<void>;
125
125
 
126
- export { type ClientGenerationOptions, type GenerationStats, HttpMethod, type ResourceRoutes, type RouteContractMapping, generateClient, groupByResource, scanContracts, watchAndGenerate };
126
+ /**
127
+ * Generator Interface
128
+ *
129
+ * Defines the contract for code generators that can be orchestrated by the codegen system.
130
+ */
131
+ interface GeneratorOptions {
132
+ /** Project root directory */
133
+ cwd: string;
134
+ /** Enable debug logging */
135
+ debug?: boolean;
136
+ /** Custom configuration options */
137
+ [key: string]: any;
138
+ }
139
+ interface Generator {
140
+ /** Unique generator name */
141
+ name: string;
142
+ /** File patterns to watch (glob patterns) */
143
+ watchPatterns: string[];
144
+ /**
145
+ * Generate code once
146
+ *
147
+ * @param options - Generator options
148
+ */
149
+ generate(options: GeneratorOptions): Promise<void>;
150
+ /**
151
+ * Handle individual file changes (optional)
152
+ *
153
+ * If not provided, the orchestrator will call generate() on any file change.
154
+ *
155
+ * @param filePath - Changed file path (relative to cwd)
156
+ * @param event - Type of file event
157
+ */
158
+ onFileChange?(filePath: string, event: 'add' | 'change' | 'unlink'): Promise<void>;
159
+ }
160
+
161
+ /**
162
+ * Codegen Orchestrator
163
+ *
164
+ * Manages multiple code generators and coordinates their execution
165
+ */
166
+
167
+ interface OrchestratorOptions {
168
+ /** List of generators to orchestrate */
169
+ generators: Generator[];
170
+ /** Project root directory */
171
+ cwd?: string;
172
+ /** Enable debug logging */
173
+ debug?: boolean;
174
+ }
175
+ declare class CodegenOrchestrator {
176
+ private readonly generators;
177
+ private readonly cwd;
178
+ private readonly debug;
179
+ private isGenerating;
180
+ private pendingRegenerations;
181
+ constructor(options: OrchestratorOptions);
182
+ /**
183
+ * Run all generators once
184
+ */
185
+ generateAll(): Promise<void>;
186
+ /**
187
+ * Start watch mode
188
+ */
189
+ watch(): Promise<void>;
190
+ }
191
+
192
+ /**
193
+ * Contract Generator
194
+ *
195
+ * Generates type-safe API client from contract definitions
196
+ */
197
+
198
+ interface ContractGeneratorConfig {
199
+ /** Routes directory (default: src/server/routes) */
200
+ routesDir?: string;
201
+ /** Output path (default: src/lib/api.ts) */
202
+ outputPath?: string;
203
+ /** Base URL for API client */
204
+ baseUrl?: string;
205
+ }
206
+ declare function createContractGenerator(config?: ContractGeneratorConfig): Generator;
207
+
208
+ /**
209
+ * Codegen Configuration Loader
210
+ *
211
+ * Loads codegen configuration from .spfnrc.json or package.json
212
+ */
213
+
214
+ interface CodegenConfig {
215
+ generators?: {
216
+ contract?: ContractGeneratorConfig & {
217
+ enabled?: boolean;
218
+ };
219
+ };
220
+ }
221
+ /**
222
+ * Load codegen configuration from .spfnrc.json or package.json
223
+ */
224
+ declare function loadCodegenConfig(cwd: string): CodegenConfig;
225
+ /**
226
+ * Create generator instances from configuration
227
+ */
228
+ declare function createGeneratorsFromConfig(config: CodegenConfig): Generator[];
229
+
230
+ export { type ClientGenerationOptions, type CodegenConfig, CodegenOrchestrator, type ContractGeneratorConfig, type GenerationStats, type Generator, type GeneratorOptions, HttpMethod, type OrchestratorOptions, type ResourceRoutes, type RouteContractMapping, createContractGenerator, createGeneratorsFromConfig, generateClient, groupByResource, loadCodegenConfig, scanContracts, watchAndGenerate };
@@ -1,9 +1,10 @@
1
1
  import { mkdir, writeFile, readdir, stat } from 'fs/promises';
2
- import { join, dirname } from 'path';
2
+ import { join, dirname, relative } from 'path';
3
3
  import * as ts from 'typescript';
4
4
  import { existsSync, mkdirSync, createWriteStream, readFileSync } from 'fs';
5
5
  import { watch } from 'chokidar';
6
6
  import pino from 'pino';
7
+ import mm from 'micromatch';
7
8
 
8
9
  // src/codegen/contract-scanner.ts
9
10
  async function scanContracts(routesDir) {
@@ -1009,7 +1010,214 @@ async function watchAndGenerate(options = {}) {
1009
1010
  if (import.meta.url === `file://${process.argv[1]}`) {
1010
1011
  watchAndGenerate({ debug: true });
1011
1012
  }
1013
+ var orchestratorLogger = logger.child("orchestrator");
1014
+ var CodegenOrchestrator = class {
1015
+ generators;
1016
+ cwd;
1017
+ debug;
1018
+ isGenerating = false;
1019
+ pendingRegenerations = /* @__PURE__ */ new Set();
1020
+ constructor(options) {
1021
+ this.generators = options.generators;
1022
+ this.cwd = options.cwd ?? process.cwd();
1023
+ this.debug = options.debug ?? false;
1024
+ }
1025
+ /**
1026
+ * Run all generators once
1027
+ */
1028
+ async generateAll() {
1029
+ if (this.debug) {
1030
+ orchestratorLogger.info("Running all generators", {
1031
+ count: this.generators.length,
1032
+ names: this.generators.map((g) => g.name)
1033
+ });
1034
+ }
1035
+ for (const generator of this.generators) {
1036
+ try {
1037
+ const genOptions = {
1038
+ cwd: this.cwd,
1039
+ debug: this.debug
1040
+ };
1041
+ await generator.generate(genOptions);
1042
+ if (this.debug) {
1043
+ orchestratorLogger.info(`[${generator.name}] Generated successfully`);
1044
+ }
1045
+ } catch (error) {
1046
+ const err = error instanceof Error ? error : new Error(String(error));
1047
+ orchestratorLogger.error(`[${generator.name}] Generation failed`, err);
1048
+ }
1049
+ }
1050
+ }
1051
+ /**
1052
+ * Start watch mode
1053
+ */
1054
+ async watch() {
1055
+ await this.generateAll();
1056
+ const allPatterns = this.generators.flatMap((g) => g.watchPatterns);
1057
+ if (allPatterns.length === 0) {
1058
+ orchestratorLogger.warn("No watch patterns defined, exiting watch mode");
1059
+ return;
1060
+ }
1061
+ const dirsToWatch = /* @__PURE__ */ new Set();
1062
+ for (const pattern of allPatterns) {
1063
+ const baseDir = pattern.split("**")[0].replace(/\/$/, "") || ".";
1064
+ dirsToWatch.add(join(this.cwd, baseDir));
1065
+ }
1066
+ const watchDirs = Array.from(dirsToWatch);
1067
+ if (this.debug) {
1068
+ orchestratorLogger.info("Starting watch mode", {
1069
+ patterns: allPatterns,
1070
+ watchDirs,
1071
+ cwd: this.cwd
1072
+ });
1073
+ }
1074
+ const watcher = watch(watchDirs, {
1075
+ ignored: /(^|[\/\\])\../,
1076
+ // ignore dotfiles
1077
+ persistent: true,
1078
+ ignoreInitial: true,
1079
+ awaitWriteFinish: {
1080
+ stabilityThreshold: 100,
1081
+ pollInterval: 50
1082
+ }
1083
+ });
1084
+ const handleChange = async (absolutePath, event) => {
1085
+ const filePath = relative(this.cwd, absolutePath);
1086
+ if (this.isGenerating) {
1087
+ this.pendingRegenerations.add(absolutePath);
1088
+ return;
1089
+ }
1090
+ this.isGenerating = true;
1091
+ this.pendingRegenerations.clear();
1092
+ if (this.debug) {
1093
+ orchestratorLogger.info(`File ${event}`, { file: filePath });
1094
+ }
1095
+ for (const generator of this.generators) {
1096
+ const matches = generator.watchPatterns.some(
1097
+ (pattern) => mm.isMatch(filePath, pattern)
1098
+ );
1099
+ if (matches) {
1100
+ try {
1101
+ if (generator.onFileChange) {
1102
+ await generator.onFileChange(filePath, event);
1103
+ } else {
1104
+ const genOptions = {
1105
+ cwd: this.cwd,
1106
+ debug: this.debug
1107
+ };
1108
+ await generator.generate(genOptions);
1109
+ }
1110
+ if (this.debug) {
1111
+ orchestratorLogger.info(`[${generator.name}] Regenerated`);
1112
+ }
1113
+ } catch (error) {
1114
+ const err = error instanceof Error ? error : new Error(String(error));
1115
+ orchestratorLogger.error(`[${generator.name}] Regeneration failed`, err);
1116
+ }
1117
+ }
1118
+ }
1119
+ this.isGenerating = false;
1120
+ if (this.pendingRegenerations.size > 0) {
1121
+ const next = Array.from(this.pendingRegenerations)[0];
1122
+ await handleChange(next, "change");
1123
+ }
1124
+ };
1125
+ watcher.on("add", (path) => handleChange(path, "add")).on("change", (path) => handleChange(path, "change")).on("unlink", (path) => handleChange(path, "unlink"));
1126
+ process.on("SIGINT", () => {
1127
+ if (this.debug) {
1128
+ orchestratorLogger.info("Shutting down watch mode");
1129
+ }
1130
+ watcher.close();
1131
+ process.exit(0);
1132
+ });
1133
+ await new Promise(() => {
1134
+ });
1135
+ }
1136
+ };
1137
+ var contractLogger = logger.child("contract-gen");
1138
+ function createContractGenerator(config = {}) {
1139
+ return {
1140
+ name: "contract",
1141
+ watchPatterns: [config.routesDir ?? "src/server/routes/**/*.ts"],
1142
+ async generate(options) {
1143
+ const cwd = options.cwd;
1144
+ const routesDir = config.routesDir ?? join(cwd, "src", "server", "routes");
1145
+ const outputPath = config.outputPath ?? join(cwd, "src", "lib", "api.ts");
1146
+ try {
1147
+ const contracts = await scanContracts(routesDir);
1148
+ if (contracts.length === 0) {
1149
+ if (options.debug) {
1150
+ contractLogger.warn("No contracts found");
1151
+ }
1152
+ return;
1153
+ }
1154
+ const stats = await generateClient(contracts, {
1155
+ outputPath,
1156
+ includeTypes: true,
1157
+ includeJsDoc: true
1158
+ });
1159
+ if (options.debug) {
1160
+ contractLogger.info("Client generated", {
1161
+ endpoints: stats.methodsGenerated,
1162
+ resources: stats.resourcesGenerated,
1163
+ duration: stats.duration
1164
+ });
1165
+ }
1166
+ } catch (error) {
1167
+ const err = error instanceof Error ? error : new Error(String(error));
1168
+ contractLogger.error("Generation failed", err);
1169
+ throw err;
1170
+ }
1171
+ }
1172
+ };
1173
+ }
1174
+ var configLogger = logger.child("config");
1175
+ function loadCodegenConfig(cwd) {
1176
+ const rcPath = join(cwd, ".spfnrc.json");
1177
+ if (existsSync(rcPath)) {
1178
+ try {
1179
+ const content = readFileSync(rcPath, "utf-8");
1180
+ const config = JSON.parse(content);
1181
+ if (config.codegen) {
1182
+ configLogger.info("Loaded config from .spfnrc.json");
1183
+ return config.codegen;
1184
+ }
1185
+ } catch (error) {
1186
+ configLogger.warn("Failed to parse .spfnrc.json", error);
1187
+ }
1188
+ }
1189
+ const pkgPath = join(cwd, "package.json");
1190
+ if (existsSync(pkgPath)) {
1191
+ try {
1192
+ const content = readFileSync(pkgPath, "utf-8");
1193
+ const pkg = JSON.parse(content);
1194
+ if (pkg.spfn?.codegen) {
1195
+ configLogger.info("Loaded config from package.json");
1196
+ return pkg.spfn.codegen;
1197
+ }
1198
+ } catch (error) {
1199
+ configLogger.warn("Failed to parse package.json", error);
1200
+ }
1201
+ }
1202
+ configLogger.info("Using default config");
1203
+ return {
1204
+ generators: {
1205
+ contract: { enabled: true }
1206
+ }
1207
+ };
1208
+ }
1209
+ function createGeneratorsFromConfig(config) {
1210
+ const generators = [];
1211
+ if (config.generators?.contract?.enabled !== false) {
1212
+ const contractConfig = {
1213
+ routesDir: config.generators?.contract?.routesDir,
1214
+ outputPath: config.generators?.contract?.outputPath};
1215
+ generators.push(createContractGenerator(contractConfig));
1216
+ configLogger.info("Contract generator enabled");
1217
+ }
1218
+ return generators;
1219
+ }
1012
1220
 
1013
- export { generateClient, groupByResource, scanContracts, watchAndGenerate };
1221
+ export { CodegenOrchestrator, createContractGenerator, createGeneratorsFromConfig, generateClient, groupByResource, loadCodegenConfig, scanContracts, watchAndGenerate };
1014
1222
  //# sourceMappingURL=index.js.map
1015
1223
  //# sourceMappingURL=index.js.map