@produtype/core 0.2.2 → 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 +49 -134
- package/dist/analyzer/analyzeProject.js +183 -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/detectAiSafety.js +21 -1
- package/dist/analyzer/detectAudit.d.ts +3 -0
- package/dist/analyzer/detectAudit.js +62 -0
- package/dist/analyzer/detectBackend.js +118 -6
- package/dist/analyzer/detectBilling.js +58 -3
- 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.d.ts +1 -0
- package/dist/analyzer/detectDatabase.js +169 -1
- package/dist/analyzer/detectEnv.js +25 -2
- package/dist/analyzer/detectErrorReporting.d.ts +3 -0
- package/dist/analyzer/detectErrorReporting.js +59 -0
- package/dist/analyzer/detectFrontend.js +9 -7
- package/dist/analyzer/detectGame.d.ts +3 -0
- package/dist/analyzer/detectGame.js +202 -0
- package/dist/analyzer/detectJobs.js +120 -13
- package/dist/analyzer/detectStack.d.ts +2 -0
- package/dist/analyzer/detectStack.js +2 -0
- package/dist/analyzer/types.d.ts +7 -0
- package/dist/api.d.ts +9 -0
- package/dist/api.js +11 -1
- package/dist/cli.js +15 -3
- package/dist/expectations/evaluateExpectations.js +18 -2
- package/dist/expectations/inferProductProfile.js +70 -33
- 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/dist/rules/rules.js +26 -8
- package/dist/utils/fileScanner.js +16 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ ProdKit is a deterministic local CLI that analyzes an existing web application r
|
|
|
4
4
|
|
|
5
5
|
It is designed for early-stage and AI-generated apps where architecture and security quality can vary significantly.
|
|
6
6
|
|
|
7
|
-
ProdKit
|
|
7
|
+
ProdKit is read-only: it never modifies the target repository.
|
|
8
8
|
|
|
9
9
|
## What ProdKit does
|
|
10
10
|
|
|
@@ -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
|
|
@@ -110,11 +110,13 @@ Optional AI layer:
|
|
|
110
110
|
- **A separate package.** The AI features live in `@produtype/ai`, which is commercial
|
|
111
111
|
and is not a dependency of this package. An open source install runs the
|
|
112
112
|
deterministic analysis only, and `--ai` reports that the package is not installed
|
|
113
|
-
rather than failing.
|
|
113
|
+
rather than failing. That package brings its own model access; nothing needs to be
|
|
114
|
+
configured here to run the analysis.
|
|
114
115
|
- ProdKit stays deterministic, offline, and read-only: nothing here makes a network
|
|
115
116
|
call.
|
|
116
|
-
-
|
|
117
|
-
|
|
117
|
+
- When `@produtype/ai` is installed, it redacts secrets from repository content before
|
|
118
|
+
sending anything, and its output stays advisory — it never changes the deterministic
|
|
119
|
+
score.
|
|
118
120
|
|
|
119
121
|
Profile warning:
|
|
120
122
|
|
|
@@ -145,145 +147,58 @@ prodkit plan ../my-app --format json
|
|
|
145
147
|
prodkit plan ../my-app --output prodkit-plan.md
|
|
146
148
|
```
|
|
147
149
|
|
|
148
|
-
## Supported stacks
|
|
150
|
+
## Supported stacks
|
|
149
151
|
|
|
150
|
-
|
|
151
|
-
- Frontend: React, Vue, Svelte, Angular, Nuxt, Vite
|
|
152
|
-
- Generic unknown app fallback
|
|
152
|
+
<!-- stacks:start -->
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
_Generated from the analyzer itself — run `npm run docs:stacks` after changing a detector._
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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.
|
|
168
190
|
|
|
169
191
|
## Current limitations
|
|
170
192
|
|
|
171
|
-
- Deterministic heuristics only
|
|
193
|
+
- Deterministic heuristics only: the AI layer is a separate package (see above)
|
|
172
194
|
- Signal-based stack coverage across common Node/Python/JS frameworks + fallback
|
|
173
195
|
- Signal-based detection can produce false positives/negatives
|
|
174
196
|
- Plan output is deterministic and read-only only
|
|
175
|
-
- No cloud dashboard
|
|
197
|
+
- No cloud dashboard or UI: this package is the CLI and the library
|
|
176
198
|
|
|
177
199
|
## Roadmap
|
|
178
200
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
3. AI explanation layer
|
|
182
|
-
4. Apply/generate PRs
|
|
183
|
-
5. Cloud GitHub integration
|
|
184
|
-
6. Framework adapters
|
|
185
|
-
7. Dashboard
|
|
186
|
-
|
|
187
|
-
## Project layout
|
|
188
|
-
|
|
189
|
-
```text
|
|
190
|
-
prodkit/
|
|
191
|
-
package.json
|
|
192
|
-
tsconfig.json
|
|
193
|
-
README.md
|
|
194
|
-
src/
|
|
195
|
-
index.ts
|
|
196
|
-
cli.ts
|
|
197
|
-
analyzer/
|
|
198
|
-
planner/
|
|
199
|
-
rules/
|
|
200
|
-
report/
|
|
201
|
-
utils/
|
|
202
|
-
tests/
|
|
203
|
-
fixtures/
|
|
204
|
-
analyzer.test.ts
|
|
205
|
-
report.test.ts
|
|
206
|
-
planner.test.ts
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
## GitHub Action
|
|
210
|
-
|
|
211
|
-
Gate a pull request on production readiness:
|
|
212
|
-
|
|
213
|
-
```yaml
|
|
214
|
-
- uses: denisbilli/prodkit@main
|
|
215
|
-
with:
|
|
216
|
-
profile: b2b-saas
|
|
217
|
-
fail-under: "65"
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
Inputs: `path`, `profile`, `fail-under`, `min-maturity`, `comment-summary`.
|
|
221
|
-
Outputs: `score`, `maturity`, `launch-ready`.
|
|
222
|
-
|
|
223
|
-
The executive summary is written to the job summary even when the thresholds fail the
|
|
224
|
-
job — a check that fails without saying why is worse than no check at all.
|
|
225
|
-
|
|
226
|
-
Requires the package to be published; until then, point the action at a local
|
|
227
|
-
checkout.
|
|
228
|
-
|
|
229
|
-
## MCP server
|
|
230
|
-
|
|
231
|
-
ProdKit ships an MCP server so an agent can assess a repository without leaving the
|
|
232
|
-
editor. It exposes the deterministic analysis only, makes no network calls, and the
|
|
233
|
-
code being analysed never leaves the machine.
|
|
234
|
-
|
|
235
|
-
Tools: `analyze_project`, `plan_remediation`, `compare_profiles`, `list_profiles`.
|
|
236
|
-
|
|
237
|
-
ProdKit is not on npm yet — the name `prodkit` is taken by an unrelated package — so
|
|
238
|
-
install it from a local checkout:
|
|
239
|
-
|
|
240
|
-
```bash
|
|
241
|
-
npm install && npm run build && npm link
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
Claude Code:
|
|
245
|
-
|
|
246
|
-
```bash
|
|
247
|
-
claude mcp add prodkit -- prodkit-mcp
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
Or, in a client that reads a JSON config:
|
|
251
|
-
|
|
252
|
-
```json
|
|
253
|
-
{
|
|
254
|
-
"mcpServers": {
|
|
255
|
-
"prodkit": {
|
|
256
|
-
"command": "prodkit-mcp"
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
`compare_profiles` is the one to reach for first: it scores the same repository
|
|
263
|
-
against every product profile in a single call, which is the question ProdKit exists
|
|
264
|
-
to answer.
|
|
265
|
-
|
|
266
|
-
## Releases
|
|
267
|
-
|
|
268
|
-
Published from CI on a version tag, with
|
|
269
|
-
[npm provenance](https://docs.npmjs.com/generating-provenance-statements): the tarball
|
|
270
|
-
is cryptographically linked to the commit and the workflow run that produced it, so it
|
|
271
|
-
can be verified rather than merely trusted. No publish token ever sits on a developer
|
|
272
|
-
machine.
|
|
273
|
-
|
|
274
|
-
To cut a release:
|
|
275
|
-
|
|
276
|
-
```bash
|
|
277
|
-
npm version patch # or minor / major — commits and tags
|
|
278
|
-
git push --follow-tags
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
The workflow refuses to publish if the tag and `package.json` disagree, and runs a
|
|
282
|
-
check that the commercial AI layer is absent from `dist` before anything leaves.
|
|
283
|
-
Release notes are generated from the commits since the previous tag.
|
|
284
|
-
|
|
285
|
-
## License
|
|
286
|
-
|
|
287
|
-
|
|
201
|
+
Shipped: the deterministic analyzer, deterministic remediation planning, and the
|
|
202
|
+
optional AI layer as `@produtype/ai`.
|
|
288
203
|
|
|
289
|
-
|
|
204
|
+
Next: applying fixes as generated pull requests, and a hosted GitHub integration.
|
|
@@ -42,6 +42,10 @@ const detectPackageManager_1 = require("./detectPackageManager");
|
|
|
42
42
|
const detectFrontend_1 = require("./detectFrontend");
|
|
43
43
|
const detectBackend_1 = require("./detectBackend");
|
|
44
44
|
const detectDatabase_1 = require("./detectDatabase");
|
|
45
|
+
const detectAudit_1 = require("./detectAudit");
|
|
46
|
+
const detectGame_1 = require("./detectGame");
|
|
47
|
+
const detectClientLogic_1 = require("./detectClientLogic");
|
|
48
|
+
const detectErrorReporting_1 = require("./detectErrorReporting");
|
|
45
49
|
const detectDocker_1 = require("./detectDocker");
|
|
46
50
|
const detectEnv_1 = require("./detectEnv");
|
|
47
51
|
const detectAuth_1 = require("./detectAuth");
|
|
@@ -134,8 +138,44 @@ function isTestOrExamplePath(file) {
|
|
|
134
138
|
|| /(^|\/)test[-_][^/]+\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file)
|
|
135
139
|
|| /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file);
|
|
136
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)$/;
|
|
137
151
|
function pickSource(files) {
|
|
138
|
-
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);
|
|
139
179
|
}
|
|
140
180
|
function pickConfig(files) {
|
|
141
181
|
return files.filter((f) => /(package\.json|tsconfig|vite\.config|docker|compose|requirements\.txt|pyproject\.toml|settings\.py|\.env)/i.test(f));
|
|
@@ -269,6 +309,119 @@ async function analyzeProject(projectPath) {
|
|
|
269
309
|
});
|
|
270
310
|
}
|
|
271
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
|
+
}
|
|
272
425
|
const npmDeps = {};
|
|
273
426
|
for (const workspace of workspaces) {
|
|
274
427
|
mergeDeps(npmDeps, workspace.packageJson?.dependencies);
|
|
@@ -298,10 +451,16 @@ async function analyzeProject(projectPath) {
|
|
|
298
451
|
files: { all: allFiles, source: sourceFiles, config: configFiles },
|
|
299
452
|
packageJson,
|
|
300
453
|
pythonDeps,
|
|
454
|
+
phpDeps: unique(phpDeps),
|
|
455
|
+
goDeps: unique(goDeps),
|
|
456
|
+
rubyDeps: unique(rubyDeps),
|
|
457
|
+
dotnetDeps: unique(dotnetDeps),
|
|
458
|
+
dotnetWebSdk,
|
|
459
|
+
dartDeps: unique(dartDeps),
|
|
301
460
|
npmDeps,
|
|
302
461
|
workspaces,
|
|
303
462
|
};
|
|
304
|
-
const [pm, frontend, backend, database, docker, env, auth, security, uploads, gdpr, billing, observability, jobs, marketplace, aiSafety, engagement, deployment] = 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([
|
|
305
464
|
(0, detectPackageManager_1.detectPackageManager)(ctx),
|
|
306
465
|
(0, detectFrontend_1.detectFrontend)(ctx),
|
|
307
466
|
(0, detectBackend_1.detectBackend)(ctx),
|
|
@@ -319,12 +478,21 @@ async function analyzeProject(projectPath) {
|
|
|
319
478
|
(0, detectAiSafety_1.detectAiSafety)(ctx),
|
|
320
479
|
(0, detectNotifications_1.detectEngagement)(ctx),
|
|
321
480
|
(0, detectDeployment_1.detectDeployment)(ctx),
|
|
481
|
+
(0, detectAudit_1.detectAudit)(ctx),
|
|
482
|
+
(0, detectGame_1.detectGame)(ctx),
|
|
483
|
+
(0, detectClientLogic_1.detectClientLogic)(ctx),
|
|
484
|
+
(0, detectErrorReporting_1.detectErrorReporting)(ctx),
|
|
322
485
|
]);
|
|
323
486
|
const detectors = (0, detectStack_1.mergeDetectors)([
|
|
324
487
|
pm.result,
|
|
325
488
|
frontend.result,
|
|
326
489
|
backend.result,
|
|
327
490
|
database.result,
|
|
491
|
+
...database.extra,
|
|
492
|
+
audit,
|
|
493
|
+
...game,
|
|
494
|
+
clientLogic,
|
|
495
|
+
errorReporting,
|
|
328
496
|
docker,
|
|
329
497
|
...env,
|
|
330
498
|
...auth,
|
|
@@ -346,9 +514,21 @@ async function analyzeProject(projectPath) {
|
|
|
346
514
|
frontend: frontend.frameworks,
|
|
347
515
|
backend: backend.frameworks,
|
|
348
516
|
databases: database.databases,
|
|
517
|
+
dataPlatforms: database.extra.find((d) => d.key === 'stack.dataPlatform')?.details?.platforms ?? [],
|
|
518
|
+
orms: database.extra.find((d) => d.key === 'stack.orm')?.details?.orms ?? [],
|
|
349
519
|
packageManager: pm.manager,
|
|
350
520
|
packageManagerConfidence: pm.confidence,
|
|
351
|
-
|
|
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
|
+
],
|
|
352
532
|
workspaces: workspaceStacks,
|
|
353
533
|
files: allFiles,
|
|
354
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;
|