@produtype/core 0.3.0 → 0.3.1
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 +39 -24
- package/dist/analyzer/analyzeProject.js +177 -3
- package/dist/analyzer/catalogue.d.ts +65 -0
- package/dist/analyzer/catalogue.js +263 -0
- package/dist/analyzer/catalogueMarkdown.d.ts +8 -0
- package/dist/analyzer/catalogueMarkdown.js +36 -0
- package/dist/analyzer/detectBackend.js +79 -23
- package/dist/analyzer/detectClientLogic.d.ts +3 -0
- package/dist/analyzer/detectClientLogic.js +88 -0
- package/dist/analyzer/detectContext.d.ts +34 -0
- package/dist/analyzer/detectContext.js +45 -0
- package/dist/analyzer/detectDatabase.js +59 -0
- package/dist/analyzer/detectErrorReporting.d.ts +3 -0
- package/dist/analyzer/detectErrorReporting.js +59 -0
- package/dist/analyzer/detectFrontend.js +9 -13
- package/dist/analyzer/detectGame.d.ts +3 -0
- package/dist/analyzer/detectGame.js +202 -0
- package/dist/api.d.ts +9 -0
- package/dist/api.js +11 -1
- package/dist/cli.js +6 -1
- package/dist/expectations/inferProductProfile.js +68 -51
- package/dist/expectations/productProfiles.d.ts +3 -0
- package/dist/expectations/productProfiles.js +110 -5
- package/dist/expectations/profileSignals.d.ts +64 -0
- package/dist/expectations/profileSignals.js +180 -0
- package/dist/expectations/types.d.ts +33 -3
- package/dist/report/buildReport.js +6 -19
- package/dist/report/executiveSummary.js +1 -0
- package/dist/report/markdownReport.js +2 -1
- package/dist/report/types.d.ts +11 -1
- package/dist/report/types.js +27 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -99,7 +99,7 @@ Options:
|
|
|
99
99
|
- `--format markdown|json` output format for the report or plan payload
|
|
100
100
|
- `--summary` print summary only
|
|
101
101
|
- `--output <path>` write output to file
|
|
102
|
-
- `--profile <name>` evaluate expected product capabilities (`static-site`, `internal-tool`, `b2c-app`, `b2b-saas`, `ai-saas`, `marketplace`, `auto`, `observed-only`)
|
|
102
|
+
- `--profile <name>` evaluate expected product capabilities (`static-site`, `internal-tool`, `b2c-app`, `b2b-saas`, `ai-saas`, `marketplace`, `game`, `client-app`, `auto`, `observed-only`)
|
|
103
103
|
- `--fail-under <score>` (analyze only) exit with code 1 if the overall score is below the threshold — useful as a CI quality gate
|
|
104
104
|
- `--min-maturity <level>` (analyze only) exit with code 1 if maturity is below `prototype|early|partial|production_ready`
|
|
105
105
|
- `--ai` (analyze only) add an AI stack/architecture insight — opt-in, advisory only, does not affect the score
|
|
@@ -149,29 +149,44 @@ prodkit plan ../my-app --output prodkit-plan.md
|
|
|
149
149
|
|
|
150
150
|
## Supported stacks
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
- **
|
|
157
|
-
|
|
158
|
-
- **
|
|
159
|
-
- **
|
|
160
|
-
|
|
161
|
-
- **ORMs:** Prisma, Drizzle, TypeORM, Sequelize, Knex, MikroORM, Kysely, SQLAlchemy,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
152
|
+
<!-- stacks:start -->
|
|
153
|
+
|
|
154
|
+
_Generated from the analyzer itself — run `npm run docs:stacks` after changing a detector._
|
|
155
|
+
|
|
156
|
+
- **Backend:** Express, Next.js, NestJS, Fastify, Hono, Elysia, Koa, AdonisJS, SvelteKit, Remix, Nuxt, Nitro, Astro, Django, Flask, FastAPI, Litestar, Sanic, Tornado, aiohttp, Starlette, Gin, Echo, Fiber, chi, Gorilla, Beego, Go, Rails, Sinatra, Hanami, Roda, Grape, Ruby, Laravel, Symfony, Slim, CodeIgniter, CakePHP, Yii, PHP, ASP.NET Core, .NET
|
|
157
|
+
- **Frontend:** React, Vite, Vue, Nuxt, Svelte, Angular, Astro, Solid, Qwik, Preact, Remix, htmx, Tailwind CSS, Electron
|
|
158
|
+
- **Mobile:** Flutter
|
|
159
|
+
- **Databases:** Postgres, MySQL, SQLite, SQL Server, MongoDB, Redis, Firestore, DynamoDB, Convex
|
|
160
|
+
- **Hosted data platforms:** Supabase, Firebase, PlanetScale, Neon, Vercel Postgres, Turso, Upstash, DynamoDB, Convex
|
|
161
|
+
- **ORMs:** Prisma, Drizzle, TypeORM, Sequelize, Knex, MikroORM, Kysely, SQLAlchemy, Tortoise, Peewee
|
|
162
|
+
|
|
163
|
+
How some of these are decided:
|
|
164
|
+
|
|
165
|
+
- **Express** — the dependency, or an import in the source
|
|
166
|
+
- **Astro** — counted as a backend only when configured to serve requests
|
|
167
|
+
- **Django** — manage.py, settings.py and urls.py together
|
|
168
|
+
- **Flask** — the dependency, or an import in the source
|
|
169
|
+
- **FastAPI** — the dependency, or an import in the source
|
|
170
|
+
- **Go** — a go.mod with no framework in it — net/http is a real answer
|
|
171
|
+
- **Ruby** — a Gemfile with no web framework in it
|
|
172
|
+
- **PHP** — PHP sources with no framework in composer.json
|
|
173
|
+
- **ASP.NET Core** — the Microsoft.NET.Sdk.Web SDK attribute
|
|
174
|
+
- **.NET** — a .csproj with no web SDK
|
|
175
|
+
- **Flutter** — pubspec.yaml — classified as a client application, not a backend
|
|
176
|
+
- **Supabase** — recorded alongside the engine it is — Postgres
|
|
177
|
+
- **Firebase** — Firestore
|
|
178
|
+
- **PlanetScale** — MySQL
|
|
179
|
+
- **Neon** — Postgres
|
|
180
|
+
- **Vercel Postgres** — Postgres
|
|
181
|
+
- **Turso** — SQLite
|
|
182
|
+
- **Upstash** — Redis
|
|
183
|
+
|
|
184
|
+
<!-- stacks:end -->
|
|
185
|
+
|
|
186
|
+
A hosted platform is recorded separately from the engine underneath it, so rules
|
|
187
|
+
written about an engine keep working without knowing about the host, while "this data
|
|
188
|
+
lives on infrastructure someone else operates" stays a question the report can ask on
|
|
189
|
+
its own.
|
|
175
190
|
|
|
176
191
|
## Current limitations
|
|
177
192
|
|
|
@@ -43,6 +43,9 @@ const detectFrontend_1 = require("./detectFrontend");
|
|
|
43
43
|
const detectBackend_1 = require("./detectBackend");
|
|
44
44
|
const detectDatabase_1 = require("./detectDatabase");
|
|
45
45
|
const detectAudit_1 = require("./detectAudit");
|
|
46
|
+
const detectGame_1 = require("./detectGame");
|
|
47
|
+
const detectClientLogic_1 = require("./detectClientLogic");
|
|
48
|
+
const detectErrorReporting_1 = require("./detectErrorReporting");
|
|
46
49
|
const detectDocker_1 = require("./detectDocker");
|
|
47
50
|
const detectEnv_1 = require("./detectEnv");
|
|
48
51
|
const detectAuth_1 = require("./detectAuth");
|
|
@@ -135,8 +138,44 @@ function isTestOrExamplePath(file) {
|
|
|
135
138
|
|| /(^|\/)test[-_][^/]+\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file)
|
|
136
139
|
|| /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file);
|
|
137
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Extensions the detectors can read.
|
|
143
|
+
*
|
|
144
|
+
* PHP, Go, Ruby, Java, C# and Rust are here because a detector that greps for a
|
|
145
|
+
* hardcoded secret, an open CORS policy or an unprotected upload does not care what
|
|
146
|
+
* language surrounds the line. Stack detection still leans on manifests, so a Laravel
|
|
147
|
+
* application is not fully understood by being readable — but being readable is the
|
|
148
|
+
* difference between a partial reading and none.
|
|
149
|
+
*/
|
|
150
|
+
const SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|mjs|cjs|py|php|go|rb|java|cs|rs|kt|swift|dart)$/;
|
|
138
151
|
function pickSource(files) {
|
|
139
|
-
return files.filter((f) =>
|
|
152
|
+
return files.filter((f) => SOURCE_EXTENSIONS.test(f) && !isTestOrExamplePath(f));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Languages present in the repository that nothing here can read.
|
|
156
|
+
*
|
|
157
|
+
* The failure this exists to stop: a published PHP application, 57 source files, was
|
|
158
|
+
* analysed from the single JavaScript file in it. The report named a backend of
|
|
159
|
+
* "unknown", raised a critical for missing authentication that is written in PHP, and
|
|
160
|
+
* said "Warnings: none" — confident, detailed, and built on two per cent of the code.
|
|
161
|
+
*
|
|
162
|
+
* Being wrong is recoverable. Being wrong while announcing no reservations is not.
|
|
163
|
+
*/
|
|
164
|
+
const KNOWN_UNREADABLE = [
|
|
165
|
+
[/\.(ex|exs)$/, 'Elixir'],
|
|
166
|
+
[/\.(scala|sc)$/, 'Scala'],
|
|
167
|
+
[/\.(clj|cljs)$/, 'Clojure'],
|
|
168
|
+
[/\.(cpp|cc|hpp)$/, 'C++'],
|
|
169
|
+
[/\.(erl|hrl)$/, 'Erlang'],
|
|
170
|
+
[/\.(hs)$/, 'Haskell'],
|
|
171
|
+
[/\.(pl|pm)$/, 'Perl'],
|
|
172
|
+
[/\.(lua)$/, 'Lua'],
|
|
173
|
+
];
|
|
174
|
+
function unreadableLanguages(files) {
|
|
175
|
+
return KNOWN_UNREADABLE
|
|
176
|
+
.map(([pattern, language]) => ({ language, files: files.filter((f) => pattern.test(f)).length }))
|
|
177
|
+
.filter((entry) => entry.files > 0)
|
|
178
|
+
.sort((a, b) => b.files - a.files);
|
|
140
179
|
}
|
|
141
180
|
function pickConfig(files) {
|
|
142
181
|
return files.filter((f) => /(package\.json|tsconfig|vite\.config|docker|compose|requirements\.txt|pyproject\.toml|settings\.py|\.env)/i.test(f));
|
|
@@ -270,6 +309,119 @@ async function analyzeProject(projectPath) {
|
|
|
270
309
|
});
|
|
271
310
|
}
|
|
272
311
|
const pythonDeps = unique(workspaces.flatMap((w) => [...w.requirementsDeps, ...w.pyprojectDeps]));
|
|
312
|
+
/**
|
|
313
|
+
* Composer requirements. A published PHP application in the corpus reported a
|
|
314
|
+
* backend of "unknown" because nothing read this file, while 57 source files sat
|
|
315
|
+
* beside it saying plainly what the project was.
|
|
316
|
+
*/
|
|
317
|
+
/**
|
|
318
|
+
* go.mod and the Gemfile, read the same way and for the same reason as composer.json:
|
|
319
|
+
* a repository whose manifest nothing here parses is reported with a backend of
|
|
320
|
+
* "unknown" while its source sits in plain sight.
|
|
321
|
+
*
|
|
322
|
+
* Both are parsed with regular expressions rather than properly. A go.mod require
|
|
323
|
+
* block and a `gem 'name'` line are simple enough that a parser would be more code
|
|
324
|
+
* than it is worth, and a manifest this cannot read yields no dependencies rather
|
|
325
|
+
* than a wrong answer.
|
|
326
|
+
*/
|
|
327
|
+
const goDeps = [];
|
|
328
|
+
for (const file of allFiles.filter((f) => /(^|\/)go\.mod$/.test(f))) {
|
|
329
|
+
const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
|
|
330
|
+
for (const line of raw.split('\n')) {
|
|
331
|
+
const match = /^\s*(?:require\s+)?([a-z0-9][\w.-]*(?:\.[a-z]{2,})?\/[\w./-]+)\s+v/i.exec(line);
|
|
332
|
+
if (match)
|
|
333
|
+
goDeps.push(match[1].toLowerCase());
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* pubspec.yaml, read for its two dependency blocks.
|
|
338
|
+
*
|
|
339
|
+
* YAML with a hand-written reader again, and the shape here is forgiving: the blocks
|
|
340
|
+
* are `dependencies:` and `dev_dependencies:`, and each entry is a name at one level
|
|
341
|
+
* of indentation. Nested constraints — a git source, an sdk pin — sit deeper and are
|
|
342
|
+
* skipped, which is right: what matters is which package is used, not where it comes
|
|
343
|
+
* from.
|
|
344
|
+
*/
|
|
345
|
+
const dartDeps = [];
|
|
346
|
+
for (const file of allFiles.filter((f) => /(^|\/)pubspec\.yaml$/.test(f))) {
|
|
347
|
+
const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
|
|
348
|
+
let inDeps = false;
|
|
349
|
+
for (const line of raw.split('\n')) {
|
|
350
|
+
if (/^(dev_)?dependencies\s*:/.test(line)) {
|
|
351
|
+
inDeps = true;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
// Any other top-level key ends the block. Without this, everything below
|
|
355
|
+
// `dependencies:` to the end of the file would be read as a package.
|
|
356
|
+
if (/^[a-z_]+\s*:/i.test(line)) {
|
|
357
|
+
inDeps = false;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (!inDeps)
|
|
361
|
+
continue;
|
|
362
|
+
const match = /^\s{2}([a-z0-9_]+)\s*:/i.exec(line);
|
|
363
|
+
if (match)
|
|
364
|
+
dartDeps.push(match[1].toLowerCase());
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* .csproj, which is XML rather than JSON or one-entry-per-line.
|
|
369
|
+
*
|
|
370
|
+
* Parsed with regular expressions like the others, and here that decision needs more
|
|
371
|
+
* defending: XML has real nesting, and a regex cannot see it. What is being read is
|
|
372
|
+
* two flat things — the Sdk attribute on the root element and the Include attribute
|
|
373
|
+
* of each PackageReference — and neither depends on where it sits in the tree. A
|
|
374
|
+
* project file this cannot read yields nothing, which is the same failure as an
|
|
375
|
+
* unreadable package.json.
|
|
376
|
+
*
|
|
377
|
+
* The Sdk attribute carries more than any dependency: `Microsoft.NET.Sdk.Web` is
|
|
378
|
+
* what makes a project a web application, and it appears in no package list.
|
|
379
|
+
*/
|
|
380
|
+
const dotnetDeps = [];
|
|
381
|
+
let dotnetWebSdk = false;
|
|
382
|
+
for (const file of allFiles.filter((f) => /\.(csproj|fsproj|vbproj)$/i.test(f))) {
|
|
383
|
+
const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
|
|
384
|
+
if (/Sdk\s*=\s*["']Microsoft\.NET\.Sdk\.Web["']/i.test(raw))
|
|
385
|
+
dotnetWebSdk = true;
|
|
386
|
+
for (const match of raw.matchAll(/<PackageReference\s+Include\s*=\s*["']([^"']+)["']/gi)) {
|
|
387
|
+
dotnetDeps.push(match[1].toLowerCase());
|
|
388
|
+
}
|
|
389
|
+
// A framework reference is how an application declares it needs the web runtime.
|
|
390
|
+
for (const match of raw.matchAll(/<FrameworkReference\s+Include\s*=\s*["']([^"']+)["']/gi)) {
|
|
391
|
+
dotnetDeps.push(match[1].toLowerCase());
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const rubyDeps = [];
|
|
395
|
+
for (const file of allFiles.filter((f) => /(^|\/)Gemfile$/.test(f))) {
|
|
396
|
+
const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
|
|
397
|
+
for (const line of raw.split('\n')) {
|
|
398
|
+
// Skip commented-out gems, which are otherwise indistinguishable from real ones.
|
|
399
|
+
if (/^\s*#/.test(line))
|
|
400
|
+
continue;
|
|
401
|
+
const match = /^\s*gem\s+['"]([^'"]+)['"]/.exec(line);
|
|
402
|
+
if (match)
|
|
403
|
+
rubyDeps.push(match[1].toLowerCase());
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const composerFiles = allFiles.filter((file) => /(^|\/)composer\.json$/.test(file));
|
|
407
|
+
const phpDeps = [];
|
|
408
|
+
for (const file of composerFiles) {
|
|
409
|
+
const raw = await (0, readTextFileSafe_1.readTextFileSafe)(root, file);
|
|
410
|
+
if (!raw)
|
|
411
|
+
continue;
|
|
412
|
+
try {
|
|
413
|
+
const parsed = JSON.parse(raw);
|
|
414
|
+
const require_ = parsed?.require ?? {};
|
|
415
|
+
const requireDev = parsed?.['require-dev'] ?? {};
|
|
416
|
+
for (const name of [...Object.keys(require_), ...Object.keys(requireDev)]) {
|
|
417
|
+
phpDeps.push(name.toLowerCase());
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
// A composer.json that does not parse tells us nothing; it is not an error worth
|
|
422
|
+
// failing an analysis over.
|
|
423
|
+
}
|
|
424
|
+
}
|
|
273
425
|
const npmDeps = {};
|
|
274
426
|
for (const workspace of workspaces) {
|
|
275
427
|
mergeDeps(npmDeps, workspace.packageJson?.dependencies);
|
|
@@ -299,10 +451,16 @@ async function analyzeProject(projectPath) {
|
|
|
299
451
|
files: { all: allFiles, source: sourceFiles, config: configFiles },
|
|
300
452
|
packageJson,
|
|
301
453
|
pythonDeps,
|
|
454
|
+
phpDeps: unique(phpDeps),
|
|
455
|
+
goDeps: unique(goDeps),
|
|
456
|
+
rubyDeps: unique(rubyDeps),
|
|
457
|
+
dotnetDeps: unique(dotnetDeps),
|
|
458
|
+
dotnetWebSdk,
|
|
459
|
+
dartDeps: unique(dartDeps),
|
|
302
460
|
npmDeps,
|
|
303
461
|
workspaces,
|
|
304
462
|
};
|
|
305
|
-
const [pm, frontend, backend, database, docker, env, auth, security, uploads, gdpr, billing, observability, jobs, marketplace, aiSafety, engagement, deployment, audit] = await Promise.all([
|
|
463
|
+
const [pm, frontend, backend, database, docker, env, auth, security, uploads, gdpr, billing, observability, jobs, marketplace, aiSafety, engagement, deployment, audit, game, clientLogic, errorReporting] = await Promise.all([
|
|
306
464
|
(0, detectPackageManager_1.detectPackageManager)(ctx),
|
|
307
465
|
(0, detectFrontend_1.detectFrontend)(ctx),
|
|
308
466
|
(0, detectBackend_1.detectBackend)(ctx),
|
|
@@ -321,6 +479,9 @@ async function analyzeProject(projectPath) {
|
|
|
321
479
|
(0, detectNotifications_1.detectEngagement)(ctx),
|
|
322
480
|
(0, detectDeployment_1.detectDeployment)(ctx),
|
|
323
481
|
(0, detectAudit_1.detectAudit)(ctx),
|
|
482
|
+
(0, detectGame_1.detectGame)(ctx),
|
|
483
|
+
(0, detectClientLogic_1.detectClientLogic)(ctx),
|
|
484
|
+
(0, detectErrorReporting_1.detectErrorReporting)(ctx),
|
|
324
485
|
]);
|
|
325
486
|
const detectors = (0, detectStack_1.mergeDetectors)([
|
|
326
487
|
pm.result,
|
|
@@ -329,6 +490,9 @@ async function analyzeProject(projectPath) {
|
|
|
329
490
|
database.result,
|
|
330
491
|
...database.extra,
|
|
331
492
|
audit,
|
|
493
|
+
...game,
|
|
494
|
+
clientLogic,
|
|
495
|
+
errorReporting,
|
|
332
496
|
docker,
|
|
333
497
|
...env,
|
|
334
498
|
...auth,
|
|
@@ -354,7 +518,17 @@ async function analyzeProject(projectPath) {
|
|
|
354
518
|
orms: database.extra.find((d) => d.key === 'stack.orm')?.details?.orms ?? [],
|
|
355
519
|
packageManager: pm.manager,
|
|
356
520
|
packageManagerConfidence: pm.confidence,
|
|
357
|
-
|
|
521
|
+
/**
|
|
522
|
+
* A reading built on part of the code says so.
|
|
523
|
+
*
|
|
524
|
+
* The report for a published PHP application said "Warnings: none" while it had
|
|
525
|
+
* seen one file out of 57. Being wrong is recoverable; being wrong while
|
|
526
|
+
* announcing no reservations is not.
|
|
527
|
+
*/
|
|
528
|
+
warnings: [
|
|
529
|
+
...pm.warnings,
|
|
530
|
+
...unreadableLanguages(allFiles).map((entry) => `${entry.files} ${entry.language} files were not analysed: this reading covers only part of the repository`),
|
|
531
|
+
],
|
|
358
532
|
workspaces: workspaceStacks,
|
|
359
533
|
files: allFiles,
|
|
360
534
|
}),
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the analyzer recognises, in one place, as data.
|
|
3
|
+
*
|
|
4
|
+
* These tables used to live inside the detector functions, and the documentation of
|
|
5
|
+
* them lived in a README and on a website — three copies of the same list, two of them
|
|
6
|
+
* maintained by hand. They had already drifted apart. Anything that answers "does it
|
|
7
|
+
* work on my stack?" now reads this, and the detectors iterate the same arrays, so a
|
|
8
|
+
* framework cannot be recognised without being listed or listed without being
|
|
9
|
+
* recognised.
|
|
10
|
+
*
|
|
11
|
+
* Entries whose detection is not a dependency lookup are still listed here, with
|
|
12
|
+
* `detectedFrom` saying what actually settles it, because a catalogue that quietly
|
|
13
|
+
* omitted Express or Django would be answering the question wrongly in the direction
|
|
14
|
+
* that loses users.
|
|
15
|
+
*/
|
|
16
|
+
/** Node backend frameworks, each conclusive from one dependency. */
|
|
17
|
+
export declare const NODE_BACKEND_FRAMEWORKS: Array<[string, string]>;
|
|
18
|
+
/** Go frameworks, read from go.mod. */
|
|
19
|
+
export declare const GO_BACKEND_FRAMEWORKS: Array<[string, string[]]>;
|
|
20
|
+
/** Ruby frameworks, read from the Gemfile. */
|
|
21
|
+
export declare const RUBY_BACKEND_FRAMEWORKS: Array<[string, string[]]>;
|
|
22
|
+
/** PHP frameworks, read from composer.json. */
|
|
23
|
+
export declare const PHP_BACKEND_FRAMEWORKS: Array<[string, string[]]>;
|
|
24
|
+
/**
|
|
25
|
+
* Python frameworks settled by a dependency alone. Django, Flask and FastAPI are not
|
|
26
|
+
* here: each also has a source-level fallback, so each has its own block in the
|
|
27
|
+
* detector and its own entry in the catalogue below.
|
|
28
|
+
*/
|
|
29
|
+
export declare const PYTHON_BACKEND_FRAMEWORKS: Array<[string, string]>;
|
|
30
|
+
/** Frontend frameworks settled by their signature dependency. */
|
|
31
|
+
export declare const FRONTEND_FRAMEWORKS: Array<[string, string[]]>;
|
|
32
|
+
export interface CatalogueEntry {
|
|
33
|
+
/** The identifier the report uses. */
|
|
34
|
+
id: string;
|
|
35
|
+
/** What a person calls it. */
|
|
36
|
+
label: string;
|
|
37
|
+
/** What settles it, for the cases a dependency name does not. */
|
|
38
|
+
detectedFrom?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface StackCatalogue {
|
|
41
|
+
backend: CatalogueEntry[];
|
|
42
|
+
frontend: CatalogueEntry[];
|
|
43
|
+
mobile: CatalogueEntry[];
|
|
44
|
+
databases: CatalogueEntry[];
|
|
45
|
+
dataPlatforms: CatalogueEntry[];
|
|
46
|
+
orms: CatalogueEntry[];
|
|
47
|
+
languages: CatalogueEntry[];
|
|
48
|
+
}
|
|
49
|
+
export declare function labelFor(id: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Whether this id has a name written for it, rather than falling back to itself.
|
|
52
|
+
*
|
|
53
|
+
* Exists for the test: several of these are genuinely lowercase — aiohttp, chi, htmx —
|
|
54
|
+
* so "the label looks like an id" cannot tell a missing name from a correct one. A new
|
|
55
|
+
* framework added to a table with no label is the drift worth catching, and this is
|
|
56
|
+
* what catches it.
|
|
57
|
+
*/
|
|
58
|
+
export declare function hasLabel(id: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Everything the analyzer can name, as a catalogue meant to be read by a person.
|
|
61
|
+
*
|
|
62
|
+
* Built from the same arrays the detectors iterate, plus explicit entries for the
|
|
63
|
+
* frameworks whose detection is not a dependency lookup.
|
|
64
|
+
*/
|
|
65
|
+
export declare function supportedStacks(): StackCatalogue;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* What the analyzer recognises, in one place, as data.
|
|
4
|
+
*
|
|
5
|
+
* These tables used to live inside the detector functions, and the documentation of
|
|
6
|
+
* them lived in a README and on a website — three copies of the same list, two of them
|
|
7
|
+
* maintained by hand. They had already drifted apart. Anything that answers "does it
|
|
8
|
+
* work on my stack?" now reads this, and the detectors iterate the same arrays, so a
|
|
9
|
+
* framework cannot be recognised without being listed or listed without being
|
|
10
|
+
* recognised.
|
|
11
|
+
*
|
|
12
|
+
* Entries whose detection is not a dependency lookup are still listed here, with
|
|
13
|
+
* `detectedFrom` saying what actually settles it, because a catalogue that quietly
|
|
14
|
+
* omitted Express or Django would be answering the question wrongly in the direction
|
|
15
|
+
* that loses users.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.FRONTEND_FRAMEWORKS = exports.PYTHON_BACKEND_FRAMEWORKS = exports.PHP_BACKEND_FRAMEWORKS = exports.RUBY_BACKEND_FRAMEWORKS = exports.GO_BACKEND_FRAMEWORKS = exports.NODE_BACKEND_FRAMEWORKS = void 0;
|
|
19
|
+
exports.labelFor = labelFor;
|
|
20
|
+
exports.hasLabel = hasLabel;
|
|
21
|
+
exports.supportedStacks = supportedStacks;
|
|
22
|
+
/** Node backend frameworks, each conclusive from one dependency. */
|
|
23
|
+
exports.NODE_BACKEND_FRAMEWORKS = [
|
|
24
|
+
['next', 'next'],
|
|
25
|
+
['nestjs', '@nestjs/core'],
|
|
26
|
+
['fastify', 'fastify'],
|
|
27
|
+
['hono', 'hono'],
|
|
28
|
+
['elysia', 'elysia'],
|
|
29
|
+
['koa', 'koa'],
|
|
30
|
+
['adonis', '@adonisjs/core'],
|
|
31
|
+
['sveltekit', '@sveltejs/kit'],
|
|
32
|
+
['remix', '@remix-run/node'],
|
|
33
|
+
['remix', '@remix-run/server-runtime'],
|
|
34
|
+
['nuxt', 'nuxt'],
|
|
35
|
+
['nitro', 'nitropack'],
|
|
36
|
+
];
|
|
37
|
+
/** Go frameworks, read from go.mod. */
|
|
38
|
+
exports.GO_BACKEND_FRAMEWORKS = [
|
|
39
|
+
['gin', ['gin-gonic/gin']],
|
|
40
|
+
['echo', ['labstack/echo', 'labstack/echo/v4']],
|
|
41
|
+
['fiber', ['gofiber/fiber', 'gofiber/fiber/v2']],
|
|
42
|
+
['chi', ['go-chi/chi', 'go-chi/chi/v5']],
|
|
43
|
+
['gorilla', ['gorilla/mux']],
|
|
44
|
+
['beego', ['beego/beego']],
|
|
45
|
+
];
|
|
46
|
+
/** Ruby frameworks, read from the Gemfile. */
|
|
47
|
+
exports.RUBY_BACKEND_FRAMEWORKS = [
|
|
48
|
+
['rails', ['rails']],
|
|
49
|
+
['sinatra', ['sinatra']],
|
|
50
|
+
['hanami', ['hanami']],
|
|
51
|
+
['roda', ['roda']],
|
|
52
|
+
['grape', ['grape']],
|
|
53
|
+
];
|
|
54
|
+
/** PHP frameworks, read from composer.json. */
|
|
55
|
+
exports.PHP_BACKEND_FRAMEWORKS = [
|
|
56
|
+
['laravel', ['laravel/framework', 'laravel/laravel']],
|
|
57
|
+
['symfony', ['symfony/framework-bundle', 'symfony/symfony']],
|
|
58
|
+
['slim', ['slim/slim']],
|
|
59
|
+
['codeigniter', ['codeigniter4/framework']],
|
|
60
|
+
['cakephp', ['cakephp/cakephp']],
|
|
61
|
+
['yii', ['yiisoft/yii2']],
|
|
62
|
+
];
|
|
63
|
+
/**
|
|
64
|
+
* Python frameworks settled by a dependency alone. Django, Flask and FastAPI are not
|
|
65
|
+
* here: each also has a source-level fallback, so each has its own block in the
|
|
66
|
+
* detector and its own entry in the catalogue below.
|
|
67
|
+
*/
|
|
68
|
+
exports.PYTHON_BACKEND_FRAMEWORKS = [
|
|
69
|
+
['litestar', 'litestar'],
|
|
70
|
+
['sanic', 'sanic'],
|
|
71
|
+
['tornado', 'tornado'],
|
|
72
|
+
['aiohttp', 'aiohttp'],
|
|
73
|
+
['starlette', 'starlette'],
|
|
74
|
+
];
|
|
75
|
+
/** Frontend frameworks settled by their signature dependency. */
|
|
76
|
+
exports.FRONTEND_FRAMEWORKS = [
|
|
77
|
+
['vue', ['vue']],
|
|
78
|
+
['nuxt', ['nuxt']],
|
|
79
|
+
['svelte', ['svelte', '@sveltejs/kit']],
|
|
80
|
+
['angular', ['@angular/core']],
|
|
81
|
+
['astro', ['astro']],
|
|
82
|
+
['solid', ['solid-js']],
|
|
83
|
+
['qwik', ['@builder.io/qwik']],
|
|
84
|
+
['preact', ['preact']],
|
|
85
|
+
['remix', ['@remix-run/react']],
|
|
86
|
+
['htmx', ['htmx.org']],
|
|
87
|
+
];
|
|
88
|
+
/** Display names for ids the tables above produce. */
|
|
89
|
+
const LABELS = {
|
|
90
|
+
next: 'Next.js',
|
|
91
|
+
nestjs: 'NestJS',
|
|
92
|
+
fastify: 'Fastify',
|
|
93
|
+
hono: 'Hono',
|
|
94
|
+
elysia: 'Elysia',
|
|
95
|
+
koa: 'Koa',
|
|
96
|
+
adonis: 'AdonisJS',
|
|
97
|
+
sveltekit: 'SvelteKit',
|
|
98
|
+
remix: 'Remix',
|
|
99
|
+
nuxt: 'Nuxt',
|
|
100
|
+
nitro: 'Nitro',
|
|
101
|
+
gin: 'Gin',
|
|
102
|
+
echo: 'Echo',
|
|
103
|
+
fiber: 'Fiber',
|
|
104
|
+
chi: 'chi',
|
|
105
|
+
gorilla: 'Gorilla',
|
|
106
|
+
beego: 'Beego',
|
|
107
|
+
rails: 'Rails',
|
|
108
|
+
sinatra: 'Sinatra',
|
|
109
|
+
hanami: 'Hanami',
|
|
110
|
+
roda: 'Roda',
|
|
111
|
+
grape: 'Grape',
|
|
112
|
+
laravel: 'Laravel',
|
|
113
|
+
symfony: 'Symfony',
|
|
114
|
+
slim: 'Slim',
|
|
115
|
+
codeigniter: 'CodeIgniter',
|
|
116
|
+
cakephp: 'CakePHP',
|
|
117
|
+
yii: 'Yii',
|
|
118
|
+
litestar: 'Litestar',
|
|
119
|
+
sanic: 'Sanic',
|
|
120
|
+
tornado: 'Tornado',
|
|
121
|
+
aiohttp: 'aiohttp',
|
|
122
|
+
starlette: 'Starlette',
|
|
123
|
+
vue: 'Vue',
|
|
124
|
+
svelte: 'Svelte',
|
|
125
|
+
angular: 'Angular',
|
|
126
|
+
astro: 'Astro',
|
|
127
|
+
solid: 'Solid',
|
|
128
|
+
qwik: 'Qwik',
|
|
129
|
+
preact: 'Preact',
|
|
130
|
+
htmx: 'htmx',
|
|
131
|
+
react: 'React',
|
|
132
|
+
vite: 'Vite',
|
|
133
|
+
tailwindcss: 'Tailwind CSS',
|
|
134
|
+
electron: 'Electron',
|
|
135
|
+
flutter: 'Flutter',
|
|
136
|
+
express: 'Express',
|
|
137
|
+
flask: 'Flask',
|
|
138
|
+
fastapi: 'FastAPI',
|
|
139
|
+
django: 'Django',
|
|
140
|
+
'aspnet-core': 'ASP.NET Core',
|
|
141
|
+
dotnet: '.NET',
|
|
142
|
+
go: 'Go',
|
|
143
|
+
ruby: 'Ruby',
|
|
144
|
+
php: 'PHP',
|
|
145
|
+
postgres: 'Postgres',
|
|
146
|
+
mysql: 'MySQL',
|
|
147
|
+
sqlite: 'SQLite',
|
|
148
|
+
sqlserver: 'SQL Server',
|
|
149
|
+
mongodb: 'MongoDB',
|
|
150
|
+
redis: 'Redis',
|
|
151
|
+
firestore: 'Firestore',
|
|
152
|
+
dynamodb: 'DynamoDB',
|
|
153
|
+
convex: 'Convex',
|
|
154
|
+
supabase: 'Supabase',
|
|
155
|
+
firebase: 'Firebase',
|
|
156
|
+
planetscale: 'PlanetScale',
|
|
157
|
+
neon: 'Neon',
|
|
158
|
+
'vercel-postgres': 'Vercel Postgres',
|
|
159
|
+
turso: 'Turso',
|
|
160
|
+
upstash: 'Upstash',
|
|
161
|
+
prisma: 'Prisma',
|
|
162
|
+
drizzle: 'Drizzle',
|
|
163
|
+
typeorm: 'TypeORM',
|
|
164
|
+
sequelize: 'Sequelize',
|
|
165
|
+
knex: 'Knex',
|
|
166
|
+
'mikro-orm': 'MikroORM',
|
|
167
|
+
kysely: 'Kysely',
|
|
168
|
+
sqlalchemy: 'SQLAlchemy',
|
|
169
|
+
tortoise: 'Tortoise',
|
|
170
|
+
peewee: 'Peewee',
|
|
171
|
+
};
|
|
172
|
+
function labelFor(id) {
|
|
173
|
+
return LABELS[id] ?? id;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Whether this id has a name written for it, rather than falling back to itself.
|
|
177
|
+
*
|
|
178
|
+
* Exists for the test: several of these are genuinely lowercase — aiohttp, chi, htmx —
|
|
179
|
+
* so "the label looks like an id" cannot tell a missing name from a correct one. A new
|
|
180
|
+
* framework added to a table with no label is the drift worth catching, and this is
|
|
181
|
+
* what catches it.
|
|
182
|
+
*/
|
|
183
|
+
function hasLabel(id) {
|
|
184
|
+
return id in LABELS;
|
|
185
|
+
}
|
|
186
|
+
function entries(ids) {
|
|
187
|
+
// Deduplicated because a framework can be reached by more than one dependency:
|
|
188
|
+
// Remix is two packages, and the catalogue is a list of frameworks, not of packages.
|
|
189
|
+
const seen = new Set();
|
|
190
|
+
return ids
|
|
191
|
+
.filter((id) => (seen.has(id) ? false : (seen.add(id), true)))
|
|
192
|
+
.map((id) => ({ id, label: labelFor(id) }));
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Everything the analyzer can name, as a catalogue meant to be read by a person.
|
|
196
|
+
*
|
|
197
|
+
* Built from the same arrays the detectors iterate, plus explicit entries for the
|
|
198
|
+
* frameworks whose detection is not a dependency lookup.
|
|
199
|
+
*/
|
|
200
|
+
function supportedStacks() {
|
|
201
|
+
return {
|
|
202
|
+
backend: [
|
|
203
|
+
{ id: 'express', label: 'Express', detectedFrom: 'the dependency, or an import in the source' },
|
|
204
|
+
...entries(exports.NODE_BACKEND_FRAMEWORKS.map(([id]) => id)),
|
|
205
|
+
{
|
|
206
|
+
id: 'astro',
|
|
207
|
+
label: 'Astro',
|
|
208
|
+
detectedFrom: 'counted as a backend only when configured to serve requests',
|
|
209
|
+
},
|
|
210
|
+
{ id: 'django', label: 'Django', detectedFrom: 'manage.py, settings.py and urls.py together' },
|
|
211
|
+
{ id: 'flask', label: 'Flask', detectedFrom: 'the dependency, or an import in the source' },
|
|
212
|
+
{ id: 'fastapi', label: 'FastAPI', detectedFrom: 'the dependency, or an import in the source' },
|
|
213
|
+
...entries(exports.PYTHON_BACKEND_FRAMEWORKS.map(([id]) => id)),
|
|
214
|
+
...entries(exports.GO_BACKEND_FRAMEWORKS.map(([id]) => id)),
|
|
215
|
+
{ id: 'go', label: 'Go', detectedFrom: 'a go.mod with no framework in it — net/http is a real answer' },
|
|
216
|
+
...entries(exports.RUBY_BACKEND_FRAMEWORKS.map(([id]) => id)),
|
|
217
|
+
{ id: 'ruby', label: 'Ruby', detectedFrom: 'a Gemfile with no web framework in it' },
|
|
218
|
+
...entries(exports.PHP_BACKEND_FRAMEWORKS.map(([id]) => id)),
|
|
219
|
+
{ id: 'php', label: 'PHP', detectedFrom: 'PHP sources with no framework in composer.json' },
|
|
220
|
+
{ id: 'aspnet-core', label: 'ASP.NET Core', detectedFrom: 'the Microsoft.NET.Sdk.Web SDK attribute' },
|
|
221
|
+
{ id: 'dotnet', label: '.NET', detectedFrom: 'a .csproj with no web SDK' },
|
|
222
|
+
],
|
|
223
|
+
frontend: [
|
|
224
|
+
{ id: 'react', label: 'React' },
|
|
225
|
+
{ id: 'vite', label: 'Vite' },
|
|
226
|
+
...entries(exports.FRONTEND_FRAMEWORKS.map(([id]) => id)),
|
|
227
|
+
{ id: 'tailwindcss', label: 'Tailwind CSS' },
|
|
228
|
+
{ id: 'electron', label: 'Electron' },
|
|
229
|
+
],
|
|
230
|
+
mobile: [
|
|
231
|
+
{
|
|
232
|
+
id: 'flutter',
|
|
233
|
+
label: 'Flutter',
|
|
234
|
+
detectedFrom: 'pubspec.yaml — classified as a client application, not a backend',
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
databases: entries(['postgres', 'mysql', 'sqlite', 'sqlserver', 'mongodb', 'redis', 'firestore', 'dynamodb', 'convex']),
|
|
238
|
+
dataPlatforms: [
|
|
239
|
+
{ id: 'supabase', label: 'Supabase', detectedFrom: 'recorded alongside the engine it is — Postgres' },
|
|
240
|
+
{ id: 'firebase', label: 'Firebase', detectedFrom: 'Firestore' },
|
|
241
|
+
{ id: 'planetscale', label: 'PlanetScale', detectedFrom: 'MySQL' },
|
|
242
|
+
{ id: 'neon', label: 'Neon', detectedFrom: 'Postgres' },
|
|
243
|
+
{ id: 'vercel-postgres', label: 'Vercel Postgres', detectedFrom: 'Postgres' },
|
|
244
|
+
{ id: 'turso', label: 'Turso', detectedFrom: 'SQLite' },
|
|
245
|
+
{ id: 'upstash', label: 'Upstash', detectedFrom: 'Redis' },
|
|
246
|
+
{ id: 'dynamodb', label: 'DynamoDB' },
|
|
247
|
+
{ id: 'convex', label: 'Convex' },
|
|
248
|
+
],
|
|
249
|
+
orms: entries([
|
|
250
|
+
'prisma',
|
|
251
|
+
'drizzle',
|
|
252
|
+
'typeorm',
|
|
253
|
+
'sequelize',
|
|
254
|
+
'knex',
|
|
255
|
+
'mikro-orm',
|
|
256
|
+
'kysely',
|
|
257
|
+
'sqlalchemy',
|
|
258
|
+
'tortoise',
|
|
259
|
+
'peewee',
|
|
260
|
+
]),
|
|
261
|
+
languages: entries(['typescript', 'javascript', 'python', 'php', 'go', 'ruby', 'java', 'csharp', 'rust', 'kotlin', 'swift', 'dart']).map((entry) => ({ ...entry, label: labelFor(entry.id) === entry.id ? entry.id.replace(/^\w/, (c) => c.toUpperCase()) : labelFor(entry.id) })),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The catalogue rendered as the README's "Supported stacks" section.
|
|
3
|
+
*
|
|
4
|
+
* Lives in the source rather than in the script that writes it, so a test can render
|
|
5
|
+
* it and compare against the file. That is what keeps the README from drifting: the
|
|
6
|
+
* list is generated, and the check that it was regenerated is part of the suite.
|
|
7
|
+
*/
|
|
8
|
+
export declare function renderSupportedStacksMarkdown(): string;
|