@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 +23 -3
- package/dist/{auto-loader-C44TcLmM.d.ts → auto-loader-Pq1wY1qz.d.ts} +2 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/codegen/index.d.ts +105 -1
- package/dist/codegen/index.js +210 -2
- package/dist/codegen/index.js.map +1 -1
- package/dist/db/index.d.ts +3 -3
- package/dist/db/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +246 -9972
- package/dist/index.js.map +1 -1
- package/dist/route/index.d.ts +1 -1
- package/dist/route/index.js +6 -2
- package/dist/route/index.js.map +1 -1
- package/dist/scripts/index.js +7 -0
- package/dist/scripts/index.js.map +1 -1
- package/dist/server/index.js +246 -9972
- package/dist/server/index.js.map +1 -1
- package/package.json +19 -8
package/README.md
CHANGED
|
@@ -6,12 +6,19 @@
|
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](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:
|
|
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
|
-
|
|
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
|
/**
|
package/dist/client/index.d.ts
CHANGED
package/dist/codegen/index.d.ts
CHANGED
|
@@ -123,4 +123,108 @@ interface WatchGenerateOptions {
|
|
|
123
123
|
*/
|
|
124
124
|
declare function watchAndGenerate(options?: WatchGenerateOptions): Promise<void>;
|
|
125
125
|
|
|
126
|
-
|
|
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 };
|
package/dist/codegen/index.js
CHANGED
|
@@ -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
|