apiforge-analyzer 1.3.2 β 1.3.5
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 +165 -3
- package/dist/cjs/cli.js +197 -40
- package/dist/cjs/detect.js +34 -16
- package/dist/cjs/frameworks/fastify.js +1112 -0
- package/dist/cjs/index.js +29 -6
- package/dist/cjs/package.json +1 -1
- package/dist/esm/cli.js +197 -40
- package/dist/esm/detect.js +33 -16
- package/dist/esm/frameworks/fastify.js +1076 -0
- package/dist/esm/index.js +29 -7
- package/dist/types/detect.d.ts +7 -3
- package/dist/types/frameworks/fastify.d.ts +16 -0
- package/dist/types/index.d.ts +16 -1
- package/package.json +8 -5
package/dist/cjs/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* APIForge Analyzer β main entry point (dual ESM/CJS)
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.detectArchitectureType = exports.detectFramework = exports.uploadRoutes = exports.exportToJSON = exports.analyzeNestJS = exports.analyzeReactRouter = exports.analyzeVite = exports.analyzeNextApp = exports.analyzeNextPages = exports.analyzeExpress = void 0;
|
|
6
|
+
exports.detectArchitectureType = exports.detectFramework = exports.uploadRoutes = exports.exportToJSON = exports.analyzeFastify = exports.analyzeNestJS = exports.analyzeReactRouter = exports.analyzeVite = exports.analyzeNextApp = exports.analyzeNextPages = exports.analyzeExpress = void 0;
|
|
7
7
|
exports.analyze = analyze;
|
|
8
8
|
const fs_1 = require("fs");
|
|
9
9
|
const path_1 = require("path");
|
|
@@ -19,6 +19,8 @@ const react_router_js_1 = require("./frameworks/react-router.js");
|
|
|
19
19
|
Object.defineProperty(exports, "analyzeReactRouter", { enumerable: true, get: function () { return react_router_js_1.analyzeReactRouter; } });
|
|
20
20
|
const nestjs_js_1 = require("./frameworks/nestjs.js");
|
|
21
21
|
Object.defineProperty(exports, "analyzeNestJS", { enumerable: true, get: function () { return nestjs_js_1.analyzeNestJS; } });
|
|
22
|
+
const fastify_js_1 = require("./frameworks/fastify.js");
|
|
23
|
+
Object.defineProperty(exports, "analyzeFastify", { enumerable: true, get: function () { return fastify_js_1.analyzeFastify; } });
|
|
22
24
|
const detect_js_1 = require("./detect.js");
|
|
23
25
|
const export_js_1 = require("./export.js");
|
|
24
26
|
const uploader_js_1 = require("./uploader.js");
|
|
@@ -117,6 +119,15 @@ async function analyze(app, options) {
|
|
|
117
119
|
framework = (0, detect_js_1.detectFramework)(app).name;
|
|
118
120
|
console.log(`π¦ Detected framework: ${framework}`);
|
|
119
121
|
}
|
|
122
|
+
// Reject loudly on an unsupported framework instead of falling through to
|
|
123
|
+
// `return []` β that return value is indistinguishable from "no routes",
|
|
124
|
+
// "no API key", "debounced", etc. A caller needs to be able to tell them apart.
|
|
125
|
+
if (!detect_js_1.SUPPORTED_FRAMEWORKS.has(framework)) {
|
|
126
|
+
const msg = `Framework "${framework}" not supported yet (${[...detect_js_1.SUPPORTED_FRAMEWORKS].join(', ')})`;
|
|
127
|
+
console.error(`β APIForge: ${msg}`);
|
|
128
|
+
(0, uploader_js_1.writeStatusFile)('error', { error: msg });
|
|
129
|
+
throw new Error(msg);
|
|
130
|
+
}
|
|
120
131
|
try {
|
|
121
132
|
switch (framework) {
|
|
122
133
|
case 'nestjs':
|
|
@@ -141,6 +152,20 @@ async function analyze(app, options) {
|
|
|
141
152
|
}
|
|
142
153
|
}
|
|
143
154
|
break;
|
|
155
|
+
case 'fastify':
|
|
156
|
+
if (!app) {
|
|
157
|
+
const msg = 'Fastify app instance required β pass your app object to analyze()';
|
|
158
|
+
console.error(`β ${msg}`);
|
|
159
|
+
(0, uploader_js_1.writeStatusFile)('error', { error: msg });
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
routes = await (0, fastify_js_1.analyzeFastify)(app, {
|
|
163
|
+
scopeCatalogPath: options.scopeCatalogPath,
|
|
164
|
+
errorCatalogPath: options.errorCatalogPath,
|
|
165
|
+
envelopes: options.envelopes,
|
|
166
|
+
annotationsPath: options.annotationsPath,
|
|
167
|
+
});
|
|
168
|
+
break;
|
|
144
169
|
case 'nextjs-pages':
|
|
145
170
|
routes = await (0, nextjs_pages_js_1.analyzeNextPages)(options.nextDir || 'pages/api');
|
|
146
171
|
break;
|
|
@@ -153,11 +178,8 @@ async function analyze(app, options) {
|
|
|
153
178
|
case 'react-router':
|
|
154
179
|
routes = await (0, react_router_js_1.analyzeReactRouter)(options.nextDir || 'src/routes.tsx');
|
|
155
180
|
break;
|
|
156
|
-
default:
|
|
157
|
-
|
|
158
|
-
console.warn(`β οΈ APIForge: ${msg}`);
|
|
159
|
-
(0, uploader_js_1.writeStatusFile)('error', { error: msg });
|
|
160
|
-
return [];
|
|
181
|
+
// No default: unsupported frameworks are rejected above, before this
|
|
182
|
+
// try block, so a thrown error here isn't silently swallowed into `[]`.
|
|
161
183
|
}
|
|
162
184
|
}
|
|
163
185
|
catch (err) {
|
|
@@ -213,6 +235,7 @@ exports.default = {
|
|
|
213
235
|
analyzeExpress: express_js_1.analyzeExpress,
|
|
214
236
|
analyzeNextPages: nextjs_pages_js_1.analyzeNextPages,
|
|
215
237
|
analyzeNextApp: nextjs_app_js_1.analyzeNextApp,
|
|
238
|
+
analyzeFastify: fastify_js_1.analyzeFastify,
|
|
216
239
|
analyzeVite: vite_js_1.analyzeVite,
|
|
217
240
|
analyzeReactRouter: react_router_js_1.analyzeReactRouter,
|
|
218
241
|
analyzeNestJS: nestjs_js_1.analyzeNestJS,
|
package/dist/cjs/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"type":"commonjs"}
|
|
1
|
+
{"type":"commonjs"}
|
package/dist/esm/cli.js
CHANGED
|
@@ -215,15 +215,34 @@ async function main() {
|
|
|
215
215
|
].join('\n'), 'Running');
|
|
216
216
|
const s = spinner();
|
|
217
217
|
s.start('Starting server and analyzing routesβ¦');
|
|
218
|
-
|
|
218
|
+
// If the dev command runs under nodemon, our own STATUS_FILE/LOCK_FILE
|
|
219
|
+
// writes (both plain .json files inside the project root) are themselves
|
|
220
|
+
// inside nodemon's default watch glob β writing them mid-analysis makes
|
|
221
|
+
// nodemon restart the very server we're trying to analyze, racing the old
|
|
222
|
+
// and new process for the port (EADDRINUSE) and hanging the whole run.
|
|
223
|
+
const restoreNodemonIgnore = suppressNodemonSelfRestart(process.cwd());
|
|
224
|
+
let serverResult;
|
|
225
|
+
try {
|
|
226
|
+
serverResult = await startAndWaitForAnalysis(runCommand, 90000);
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
restoreNodemonIgnore();
|
|
230
|
+
}
|
|
219
231
|
if (serverResult.status === 'error') {
|
|
220
232
|
s.stop(red(`β ${serverResult.message}`));
|
|
221
233
|
// Give a targeted hint for common failure modes
|
|
222
|
-
if (serverResult.message.includes('API key')) {
|
|
223
|
-
log.warn(yellow('Hint:
|
|
224
|
-
log.warn(yellow('
|
|
225
|
-
log.
|
|
226
|
-
log.
|
|
234
|
+
if (serverResult.message.includes('API key') || serverResult.message.includes('Invalid API key')) {
|
|
235
|
+
log.warn(yellow('Hint: Your API key was rejected by the server. Common causes:'));
|
|
236
|
+
log.warn(yellow(''));
|
|
237
|
+
log.warn(yellow(' 1. Key not yet created β go to your dashboard and generate one:'));
|
|
238
|
+
log.info(` ${cyan('https://apiforge.brainfogagency.com/dashboard')}`);
|
|
239
|
+
log.warn(yellow(''));
|
|
240
|
+
log.warn(yellow(' 2. Key belongs to a different account β make sure you\'re logged in'));
|
|
241
|
+
log.warn(yellow(' to the same account that owns this project.'));
|
|
242
|
+
log.warn(yellow(''));
|
|
243
|
+
log.warn(yellow(' 3. Server not loading .env.local β add this at the top of your entry file:'));
|
|
244
|
+
log.info(` ${cyan("require('dotenv').config({ path: '.env.local' })")}`);
|
|
245
|
+
log.warn(yellow(' Or install dotenv: npm install dotenv'));
|
|
227
246
|
}
|
|
228
247
|
else if (serverResult.message.includes('No routes')) {
|
|
229
248
|
log.warn(yellow('Hint: Make sure all routes are registered before analyze() is called.'));
|
|
@@ -233,6 +252,14 @@ async function main() {
|
|
|
233
252
|
log.warn(yellow('Hint: Your server might be taking too long to start, or the analyzer'));
|
|
234
253
|
log.warn(yellow(' is not being called. Check your server logs for APIForge messages.'));
|
|
235
254
|
}
|
|
255
|
+
// Show captured server output to help debug the root cause
|
|
256
|
+
const capturedOutput = serverResult.serverOutput;
|
|
257
|
+
if (capturedOutput) {
|
|
258
|
+
log.info('');
|
|
259
|
+
log.info(dim('βββ Server output βββββββββββββββββββββββββββββββ'));
|
|
260
|
+
capturedOutput.split('\n').slice(0, 25).forEach((line) => log.info(dim(` ${line}`)));
|
|
261
|
+
log.info(dim('βββββββββββββββββββββββββββββββββββββββββββββββββ'));
|
|
262
|
+
}
|
|
236
263
|
await offerCleanup(entryFile, injected);
|
|
237
264
|
outro(red('Analysis failed.'));
|
|
238
265
|
process.exit(1);
|
|
@@ -633,11 +660,9 @@ async function microservicesFlow(config, opts) {
|
|
|
633
660
|
child.stderr?.on('data', capture);
|
|
634
661
|
// Wait up to 30 s for the status file to appear
|
|
635
662
|
const routes = await waitForServiceStatusFile(statusFile, 30000);
|
|
636
|
-
// Kill service process
|
|
637
|
-
|
|
638
|
-
child.
|
|
639
|
-
}
|
|
640
|
-
catch { }
|
|
663
|
+
// Kill service process (and its full descendant tree β see killProcessTree)
|
|
664
|
+
if (child.pid)
|
|
665
|
+
killProcessTree(child.pid);
|
|
641
666
|
tryUnlink(join(cwd, statusFile));
|
|
642
667
|
if (routes && routes.length > 0) {
|
|
643
668
|
const prefix = gatewayPrefixes[svc.name];
|
|
@@ -739,16 +764,103 @@ async function microservicesFlow(config, opts) {
|
|
|
739
764
|
}[approval.status];
|
|
740
765
|
outro(outroMsg);
|
|
741
766
|
}
|
|
767
|
+
// βββ Server subprocess ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
768
|
+
/**
|
|
769
|
+
* Kill the whole process tree rooted at `pid`, not just that one process.
|
|
770
|
+
*
|
|
771
|
+
* The dev server is started via `spawn(command, { shell: true })`, so `pid`
|
|
772
|
+
* is the shell (cmd.exe /c "npm run dev") β `child.kill()` only signals that
|
|
773
|
+
* shell. On Windows, that leaves the real descendants (npm β nodemon β node)
|
|
774
|
+
* running and still holding the port, so the very next run immediately hits
|
|
775
|
+
* EADDRINUSE. `taskkill /T` kills the shell's entire descendant tree instead.
|
|
776
|
+
* On POSIX this isn't needed the same way (SIGTERM propagates to the shell's
|
|
777
|
+
* job, and the analyzer process itself exits cleanly), so a plain kill there
|
|
778
|
+
* is enough.
|
|
779
|
+
*/
|
|
780
|
+
function killProcessTree(pid) {
|
|
781
|
+
if (process.platform === 'win32') {
|
|
782
|
+
try {
|
|
783
|
+
spawn('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' });
|
|
784
|
+
}
|
|
785
|
+
catch { }
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
try {
|
|
789
|
+
process.kill(pid, 'SIGTERM');
|
|
790
|
+
}
|
|
791
|
+
catch { }
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
const NODEMON_IGNORE_PATTERNS = ['.apiforge-status.json', '.apiforge-last-run.json', '.env.local'];
|
|
795
|
+
/**
|
|
796
|
+
* If the project depends on nodemon, make sure it won't watch our own
|
|
797
|
+
* .apiforge-status.json / .apiforge-last-run.json / .env.local writes β
|
|
798
|
+
* nodemon's default watch glob includes *.json, so without this it restarts
|
|
799
|
+
* the app mid-analysis (racing the old and new process for the port) every
|
|
800
|
+
* time analyze() writes its completion signal.
|
|
801
|
+
*
|
|
802
|
+
* Uses nodemon's own `.nodemonignore` file (read automatically if present,
|
|
803
|
+
* no config changes needed). Creates it if absent, or appends any missing
|
|
804
|
+
* patterns if it already exists β then returns a function that restores
|
|
805
|
+
* whatever was there before (or removes the file if we created it).
|
|
806
|
+
*/
|
|
807
|
+
function suppressNodemonSelfRestart(cwd) {
|
|
808
|
+
try {
|
|
809
|
+
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8'));
|
|
810
|
+
const usesNodemon = !!(pkg.dependencies?.nodemon || pkg.devDependencies?.nodemon);
|
|
811
|
+
if (!usesNodemon)
|
|
812
|
+
return () => { };
|
|
813
|
+
}
|
|
814
|
+
catch {
|
|
815
|
+
return () => { };
|
|
816
|
+
}
|
|
817
|
+
const ignorePath = join(cwd, '.nodemonignore');
|
|
818
|
+
const existed = existsSync(ignorePath);
|
|
819
|
+
const original = existed ? readFileSync(ignorePath, 'utf-8') : '';
|
|
820
|
+
const existingLines = new Set(original.split('\n').map(l => l.trim()).filter(Boolean));
|
|
821
|
+
const missing = NODEMON_IGNORE_PATTERNS.filter(p => !existingLines.has(p));
|
|
822
|
+
if (missing.length === 0)
|
|
823
|
+
return () => { };
|
|
824
|
+
try {
|
|
825
|
+
const next = existed
|
|
826
|
+
? original.trimEnd() + '\n' + missing.join('\n') + '\n'
|
|
827
|
+
: missing.join('\n') + '\n';
|
|
828
|
+
writeFileSync(ignorePath, next, 'utf-8');
|
|
829
|
+
}
|
|
830
|
+
catch {
|
|
831
|
+
return () => { };
|
|
832
|
+
}
|
|
833
|
+
return () => {
|
|
834
|
+
try {
|
|
835
|
+
if (existed) {
|
|
836
|
+
writeFileSync(ignorePath, original, 'utf-8');
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
unlinkSync(ignorePath);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
catch { }
|
|
843
|
+
};
|
|
844
|
+
}
|
|
742
845
|
function startAndWaitForAnalysis(command, timeoutMs) {
|
|
743
846
|
return new Promise(resolve => {
|
|
744
847
|
const statusPath = join(process.cwd(), STATUS_FILE);
|
|
745
848
|
const child = spawn(command, {
|
|
746
849
|
cwd: process.cwd(),
|
|
747
|
-
stdio: 'ignore',
|
|
850
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
748
851
|
shell: true,
|
|
749
852
|
env: { ...process.env },
|
|
750
853
|
detached: false,
|
|
751
854
|
});
|
|
855
|
+
// Capture server output (head-capped) so we can show it on failure
|
|
856
|
+
let serverOutput = '';
|
|
857
|
+
const MAX_OUTPUT = 4000;
|
|
858
|
+
const captureOutput = (chunk) => {
|
|
859
|
+
if (serverOutput.length < MAX_OUTPUT)
|
|
860
|
+
serverOutput += chunk.toString();
|
|
861
|
+
};
|
|
862
|
+
child.stdout?.on('data', captureOutput);
|
|
863
|
+
child.stderr?.on('data', captureOutput);
|
|
752
864
|
let resolved = false;
|
|
753
865
|
function finish(result) {
|
|
754
866
|
if (resolved)
|
|
@@ -756,11 +868,9 @@ function startAndWaitForAnalysis(command, timeoutMs) {
|
|
|
756
868
|
resolved = true;
|
|
757
869
|
clearInterval(poller);
|
|
758
870
|
clearTimeout(timeout);
|
|
759
|
-
|
|
760
|
-
child.
|
|
761
|
-
}
|
|
762
|
-
catch { }
|
|
763
|
-
resolve(result);
|
|
871
|
+
if (child.pid)
|
|
872
|
+
killProcessTree(child.pid);
|
|
873
|
+
resolve({ ...result, serverOutput: serverOutput.trim() });
|
|
764
874
|
}
|
|
765
875
|
// Poll for the status file every 500 ms
|
|
766
876
|
const poller = setInterval(() => {
|
|
@@ -870,6 +980,28 @@ function removeInjectedCode(filePath) {
|
|
|
870
980
|
return false;
|
|
871
981
|
}
|
|
872
982
|
}
|
|
983
|
+
// βββ Entry file detection βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
984
|
+
/**
|
|
985
|
+
* If `candidate` looks like compiled build output (dist/build/out/lib) and a
|
|
986
|
+
* TypeScript source with the same base name exists under src/, prefer that
|
|
987
|
+
* instead of the build artifact.
|
|
988
|
+
*
|
|
989
|
+
* Injecting into dist/index.js is worse than pointless when the project has
|
|
990
|
+
* a watch compiler (tsc --watch, common in a "dev" script alongside
|
|
991
|
+
* nodemon watching that same dist folder): the compiler immediately
|
|
992
|
+
* overwrites the file with a fresh, uninjected build, and that overwrite
|
|
993
|
+
* itself looks like a file change to nodemon, which restarts the server β
|
|
994
|
+
* colliding with the still-running instance that had the injected snippet
|
|
995
|
+
* and racing it for the port (EADDRINUSE). The injected code also can't be
|
|
996
|
+
* cleaned up afterward because tsc already erased it before cleanup ran.
|
|
997
|
+
*/
|
|
998
|
+
function preferSourceOverBuildOutput(cwd, candidate) {
|
|
999
|
+
const match = candidate.match(/^(?:dist|build|out|lib)[\\/](.+)\.(?:js|mjs|cjs)$/);
|
|
1000
|
+
if (!match)
|
|
1001
|
+
return candidate;
|
|
1002
|
+
const srcCandidate = join('src', `${match[1]}.ts`);
|
|
1003
|
+
return existsSync(join(cwd, srcCandidate)) ? srcCandidate : candidate;
|
|
1004
|
+
}
|
|
873
1005
|
/** Look for a server entry inside a single directory (checks src/ then root). */
|
|
874
1006
|
function findEntryInDir(absDir) {
|
|
875
1007
|
const names = ['index', 'server', 'app', 'main'];
|
|
@@ -895,12 +1027,12 @@ function detectMainEntry(cwd) {
|
|
|
895
1027
|
try {
|
|
896
1028
|
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8'));
|
|
897
1029
|
if (pkg.main && existsSync(join(cwd, pkg.main)))
|
|
898
|
-
return pkg.main;
|
|
1030
|
+
return preferSourceOverBuildOutput(cwd, pkg.main);
|
|
899
1031
|
for (const s of ['start', 'dev', 'serve']) {
|
|
900
1032
|
const cmd = pkg.scripts?.[s] ?? '';
|
|
901
|
-
const m = cmd.match(/(?:node|tsx|ts-node|nodemon)\s+([^\s&|;]+\.(js|ts|mjs|cjs))/);
|
|
1033
|
+
const m = cmd.match(/(?:node|tsx|ts-node|nodemon)\s+(?:-\S+\s+)*([^\s&|;]+\.(js|ts|mjs|cjs))/);
|
|
902
1034
|
if (m && existsSync(join(cwd, m[1])))
|
|
903
|
-
return m[1];
|
|
1035
|
+
return preferSourceOverBuildOutput(cwd, m[1]);
|
|
904
1036
|
}
|
|
905
1037
|
}
|
|
906
1038
|
catch { }
|
|
@@ -991,12 +1123,12 @@ function detectEntryFile(archType) {
|
|
|
991
1123
|
try {
|
|
992
1124
|
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8'));
|
|
993
1125
|
if (pkg.main && existsSync(join(cwd, pkg.main)))
|
|
994
|
-
return pkg.main;
|
|
1126
|
+
return preferSourceOverBuildOutput(cwd, pkg.main);
|
|
995
1127
|
for (const s of ['start', 'dev', 'serve']) {
|
|
996
1128
|
const cmd = pkg.scripts?.[s] ?? '';
|
|
997
|
-
const m = cmd.match(/(?:node|tsx|ts-node|nodemon)\s+([^\s&|;]+\.(js|ts|mjs|cjs))/);
|
|
1129
|
+
const m = cmd.match(/(?:node|tsx|ts-node|nodemon)\s+(?:-\S+\s+)*([^\s&|;]+\.(js|ts|mjs|cjs))/);
|
|
998
1130
|
if (m && existsSync(join(cwd, m[1])))
|
|
999
|
-
return m[1];
|
|
1131
|
+
return preferSourceOverBuildOutput(cwd, m[1]);
|
|
1000
1132
|
}
|
|
1001
1133
|
}
|
|
1002
1134
|
catch { }
|
|
@@ -1187,24 +1319,37 @@ function buildSnippet(projectName, apiKey, baseUrl, backendUrl, appVar, isESM, a
|
|
|
1187
1319
|
].join('\n');
|
|
1188
1320
|
// Collection mode: some services only mount routes inside an async bootstrap
|
|
1189
1321
|
// gated behind a DB/broker connect (which may hang/fail outside Docker).
|
|
1190
|
-
//
|
|
1191
|
-
//
|
|
1192
|
-
//
|
|
1322
|
+
//
|
|
1323
|
+
// Express exposes no "I'm done registering routes" signal, so for
|
|
1324
|
+
// Express-shaped apps we poll app._router.stack briefly before giving up
|
|
1325
|
+
// and collecting whatever is there β bounded well under the CLI's 30s
|
|
1326
|
+
// per-service timeout so a hung connect still surfaces.
|
|
1327
|
+
//
|
|
1328
|
+
// Fastify DOES expose a real completion signal (`app.ready()`, resolved
|
|
1329
|
+
// once every plugin/route has finished registering), so Fastify apps use
|
|
1330
|
+
// that instead of the Express-shaped poll β polling `._router.stack` on a
|
|
1331
|
+
// Fastify instance always looks "empty" (Fastify doesn't have that
|
|
1332
|
+
// property), which used to make hasRoutes evaluate true immediately and
|
|
1333
|
+
// fire analyze() before async-registered routes existed yet.
|
|
1193
1334
|
const runAnalyze = `APIForge.analyze(${appVar}, {\n${opts}\n })`;
|
|
1194
1335
|
const call = collectionOpts
|
|
1195
1336
|
? [
|
|
1196
|
-
`
|
|
1197
|
-
`
|
|
1198
|
-
`
|
|
1199
|
-
`
|
|
1200
|
-
`
|
|
1201
|
-
`
|
|
1202
|
-
`
|
|
1203
|
-
`
|
|
1204
|
-
`
|
|
1337
|
+
` if (${appVar} && typeof ${appVar}.ready === 'function') {`,
|
|
1338
|
+
` ${appVar}.ready().then(() => { ${runAnalyze} }).catch(() => { ${runAnalyze} })`,
|
|
1339
|
+
` } else {`,
|
|
1340
|
+
` let __apiforgeTries = 0`,
|
|
1341
|
+
` const __apiforgeTryCollect = () => {`,
|
|
1342
|
+
` const hasRoutes = !${appVar} || !${appVar}._router`,
|
|
1343
|
+
` ? true`,
|
|
1344
|
+
` : Array.isArray(${appVar}._router.stack) && ${appVar}._router.stack.length > 0`,
|
|
1345
|
+
` if (hasRoutes || __apiforgeTries++ >= 26) {`,
|
|
1346
|
+
` ${runAnalyze}`,
|
|
1347
|
+
` } else {`,
|
|
1348
|
+
` setTimeout(__apiforgeTryCollect, 300)`,
|
|
1349
|
+
` }`,
|
|
1205
1350
|
` }`,
|
|
1351
|
+
` __apiforgeTryCollect()`,
|
|
1206
1352
|
` }`,
|
|
1207
|
-
` __apiforgeTryCollect()`,
|
|
1208
1353
|
].join('\n')
|
|
1209
1354
|
: ` ${runAnalyze}`;
|
|
1210
1355
|
if (isESM) {
|
|
@@ -1239,11 +1384,23 @@ function detectESM(filePath, content) {
|
|
|
1239
1384
|
return /^(?:import|export)\s+/m.test(content);
|
|
1240
1385
|
}
|
|
1241
1386
|
function detectAppVariable(content) {
|
|
1387
|
+
// Optional TS type annotation between the variable name and `=`, e.g.
|
|
1388
|
+
// `const app: FastifyInstance = fastify()` β common in Fastify+TS
|
|
1389
|
+
// projects (Fastify has first-class TS typings, unlike bare Express).
|
|
1390
|
+
// Without this, none of the patterns below matched a type-annotated
|
|
1391
|
+
// Fastify app at all.
|
|
1392
|
+
const typeAnn = `(?:\\s*:\\s*[\\w.$]+(?:<[^>]*>)?)?`;
|
|
1242
1393
|
const patterns = [
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1394
|
+
new RegExp(`(?:const|let|var)\\s+(\\w+)${typeAnn}\\s*=\\s*express\\s*(?:\\.default)?\\s*\\(`),
|
|
1395
|
+
new RegExp(`(?:const|let|var)\\s+(\\w+)${typeAnn}\\s*=\\s*(?:fastify|Fastify)\\s*\\(`),
|
|
1396
|
+
new RegExp(`(?:const|let|var)\\s+(\\w+)${typeAnn}\\s*=\\s*new\\s+Koa\\s*\\(`),
|
|
1397
|
+
new RegExp(`(?:const|let|var)\\s+(\\w+)${typeAnn}\\s*=\\s*(?:Router|createApp|createServer)\\s*\\(`),
|
|
1398
|
+
// Inline `require('fastify')(...)` / `require('express')(...)` β the app
|
|
1399
|
+
// factory is never bound to a local name, only its return value is. This
|
|
1400
|
+
// is at least as common as the named-import style above (it's the pattern
|
|
1401
|
+
// in Fastify's own project generator output), and the patterns above never
|
|
1402
|
+
// match it because there's no `fastify`/`express` identifier in the source.
|
|
1403
|
+
new RegExp(`(?:const|let|var)\\s+(\\w+)${typeAnn}\\s*=\\s*require\\s*\\(\\s*['"](?:fastify|express)['"]\\s*\\)\\s*(?:\\.default\\s*)?\\(`),
|
|
1247
1404
|
];
|
|
1248
1405
|
for (const re of patterns) {
|
|
1249
1406
|
const m = content.match(re);
|
package/dist/esm/detect.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Frameworks analyze() can actually dispatch to. Detection can identify more
|
|
5
|
+
* frameworks than this (koa, hapi) β those are named so the detector is honest
|
|
6
|
+
* about what it saw, but `supported: false` tells callers not to expect output.
|
|
7
|
+
*/
|
|
8
|
+
export const SUPPORTED_FRAMEWORKS = new Set([
|
|
9
|
+
'express',
|
|
10
|
+
'fastify',
|
|
11
|
+
'nestjs',
|
|
12
|
+
'nextjs-pages',
|
|
13
|
+
'nextjs-app',
|
|
14
|
+
'vite',
|
|
15
|
+
'react-router',
|
|
16
|
+
]);
|
|
3
17
|
const cwd = () => process.cwd();
|
|
4
18
|
function fileExists(rel) {
|
|
5
19
|
return existsSync(join(cwd(), rel));
|
|
@@ -18,16 +32,19 @@ function tryResolve(mod) {
|
|
|
18
32
|
* Detect the web framework from the live app instance + filesystem hints.
|
|
19
33
|
* Confidence is used to pick the best match when multiple signals fire.
|
|
20
34
|
*/
|
|
35
|
+
function mk(name, confidence) {
|
|
36
|
+
return { name, confidence, supported: SUPPORTED_FRAMEWORKS.has(name) };
|
|
37
|
+
}
|
|
21
38
|
export function detectFramework(app) {
|
|
22
39
|
// ββ Instance-based detection (highest confidence) βββββββββββββββββββββββββ
|
|
23
40
|
if (app && typeof app === 'function') {
|
|
24
41
|
// Express: _router or lazyrouter
|
|
25
42
|
if (app._router || (app.lazyrouter && typeof app.lazyrouter === 'function')) {
|
|
26
|
-
return
|
|
43
|
+
return mk('express', 1.0);
|
|
27
44
|
}
|
|
28
45
|
// Koa: middleware array
|
|
29
46
|
if (Array.isArray(app.middleware) && typeof app.use === 'function' && app.context) {
|
|
30
|
-
return
|
|
47
|
+
return mk('koa', 0.95);
|
|
31
48
|
}
|
|
32
49
|
}
|
|
33
50
|
// Fastify: inject method and route method
|
|
@@ -35,7 +52,7 @@ export function detectFramework(app) {
|
|
|
35
52
|
typeof app === 'object' &&
|
|
36
53
|
typeof app.inject === 'function' &&
|
|
37
54
|
typeof app.route === 'function') {
|
|
38
|
-
return
|
|
55
|
+
return mk('fastify', 1.0);
|
|
39
56
|
}
|
|
40
57
|
// Hapi: server with route + start methods
|
|
41
58
|
if (app &&
|
|
@@ -43,43 +60,43 @@ export function detectFramework(app) {
|
|
|
43
60
|
typeof app.route === 'function' &&
|
|
44
61
|
typeof app.start === 'function' &&
|
|
45
62
|
typeof app.stop === 'function') {
|
|
46
|
-
return
|
|
63
|
+
return mk('hapi', 0.95);
|
|
47
64
|
}
|
|
48
65
|
// NestJS application
|
|
49
66
|
if (app &&
|
|
50
67
|
typeof app === 'object' &&
|
|
51
68
|
typeof app.getHttpServer === 'function' &&
|
|
52
69
|
typeof app.listen === 'function') {
|
|
53
|
-
return
|
|
70
|
+
return mk('nestjs', 0.95);
|
|
54
71
|
}
|
|
55
72
|
// ββ Filesystem detection ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
56
73
|
// Next.js App Router
|
|
57
74
|
if (fileExists('app') && (fileExists('app/layout.tsx') || fileExists('app/layout.js'))) {
|
|
58
|
-
return
|
|
75
|
+
return mk('nextjs-app', 0.92);
|
|
59
76
|
}
|
|
60
77
|
// Next.js Pages Router
|
|
61
78
|
if (fileExists('pages/api') || fileExists('src/pages/api')) {
|
|
62
|
-
return
|
|
79
|
+
return mk('nextjs-pages', 0.90);
|
|
63
80
|
}
|
|
64
81
|
if (fileExists('next.config.js') ||
|
|
65
82
|
fileExists('next.config.mjs') ||
|
|
66
83
|
fileExists('next.config.ts')) {
|
|
67
|
-
return
|
|
84
|
+
return mk('nextjs-pages', 0.75);
|
|
68
85
|
}
|
|
69
86
|
// ββ Dependency-based detection (lowest confidence) ββββββββββββββββββββββββ
|
|
70
87
|
if (tryResolve('express'))
|
|
71
|
-
return
|
|
88
|
+
return mk('express', 0.70);
|
|
72
89
|
if (tryResolve('fastify'))
|
|
73
|
-
return
|
|
90
|
+
return mk('fastify', 0.70);
|
|
74
91
|
if (tryResolve('koa'))
|
|
75
|
-
return
|
|
92
|
+
return mk('koa', 0.70);
|
|
76
93
|
if (tryResolve('@hapi/hapi') || tryResolve('hapi'))
|
|
77
|
-
return
|
|
94
|
+
return mk('hapi', 0.70);
|
|
78
95
|
if (tryResolve('@nestjs/core'))
|
|
79
|
-
return
|
|
96
|
+
return mk('nestjs', 0.70);
|
|
80
97
|
if (tryResolve('vite'))
|
|
81
|
-
return
|
|
98
|
+
return mk('vite', 0.60);
|
|
82
99
|
if (tryResolve('react-router-dom'))
|
|
83
|
-
return
|
|
84
|
-
return
|
|
100
|
+
return mk('react-router', 0.55);
|
|
101
|
+
return mk('unknown', 0);
|
|
85
102
|
}
|