@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 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
- - **Backend (Node):** Express, Next.js, NestJS, Fastify, Hono, Elysia, Koa, AdonisJS,
153
- SvelteKit, Remix, Nuxt, Nitro
154
- - **Backend (Python):** Django, FastAPI, Flask, Litestar, Starlette, Sanic, Tornado,
155
- aiohttp
156
- - **Frontend:** React, Vue, Svelte, Angular, Solid, Qwik, Preact, Astro, Nuxt, Remix,
157
- Vite, Tailwind, htmx
158
- - **Databases:** Postgres, MySQL, SQLite, MongoDB, Redis
159
- - **Hosted data platforms:** Supabase, Firebase, Neon, PlanetScale, Vercel Postgres,
160
- Turso, Upstash, DynamoDB, Convex
161
- - **ORMs:** Prisma, Drizzle, TypeORM, Sequelize, Knex, MikroORM, Kysely, SQLAlchemy,
162
- Tortoise, Peewee
163
- - Generic unknown app fallback
164
-
165
- Two of these distinctions are deliberate rather than incidental.
166
-
167
- A hosted platform is recorded separately from the engine underneath it. Supabase is
168
- Postgres and Turso is SQLite, so rules written about an engine keep working without
169
- knowing about the host, while "this data lives on infrastructure someone else
170
- operates" stays a question the report can ask on its own.
171
-
172
- Astro counts as a backend only when it is configured to serve requests — `output` set
173
- to `server` or `hybrid`, or an adapter installed. A static Astro site is a static
174
- site, and is not marked down for missing the things an application needs.
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) => /\.(ts|tsx|js|jsx|mjs|cjs|py)$/.test(f) && !isTestOrExamplePath(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
- warnings: pm.warnings,
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;