@produtype/core 0.78.0 → 0.80.0
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
|
@@ -176,7 +176,7 @@ _Generated from the analyzer itself — run `npm run docs:stacks` after changing
|
|
|
176
176
|
|
|
177
177
|
- **Backend:** Express, Next.js, NestJS, Fastify, Hono, Elysia, Koa, AdonisJS, SvelteKit, Remix, Nuxt, Nitro, Astro, Django, Flask, FastAPI, aiohttp, Litestar, Sanic, Tornado, Starlette, Streamlit, Gradio, Dash, Chainlit, Gin, Echo, Fiber, chi, Gorilla, Beego, Go, Axum, Actix Web, Rocket, Warp, Tide, Poem, Salvo, Tower HTTP, Hyper, Spring Boot, Quarkus, Micronaut, Ktor, Javalin, Vert.x, Dropwizard, Helidon, Rails, Sinatra, Hanami, Roda, Grape, Ruby, Laravel, Symfony, Slim, CodeIgniter, CakePHP, Yii, PHP, ASP.NET Core, .NET
|
|
178
178
|
- **Frontend:** React, Vite, Vue, Nuxt, Svelte, Angular, Astro, Solid, Qwik, Preact, Remix, htmx, Tailwind CSS, Electron
|
|
179
|
-
- **Mobile:** Flutter, React Native, iOS (native), Android (native)
|
|
179
|
+
- **Mobile:** Flutter, React Native, iOS (native), Android (native), SwiftUI, UIKit, Jetpack Compose, Android views
|
|
180
180
|
- **Databases:** Postgres, MySQL, SQLite, SQL Server, MongoDB, Redis, Firestore, DynamoDB, Convex
|
|
181
181
|
- **Hosted data platforms:** Supabase, Firebase, PlanetScale, Neon, Vercel Postgres, Turso, Upstash, DynamoDB, Convex
|
|
182
182
|
- **ORMs:** Prisma, Drizzle, TypeORM, Sequelize, Knex, MikroORM, Kysely, SQLAlchemy, Tortoise, Peewee
|
|
@@ -198,6 +198,10 @@ How some of these are decided:
|
|
|
198
198
|
- **React Native** — the react-native or expo dependency
|
|
199
199
|
- **iOS (native)** — Info.plist, Package.swift, a Podfile or an .xcodeproj in the tree
|
|
200
200
|
- **Android (native)** — AndroidManifest.xml, or build.gradle in either dialect
|
|
201
|
+
- **SwiftUI** — import SwiftUI
|
|
202
|
+
- **UIKit** — import UIKit
|
|
203
|
+
- **Jetpack Compose** — import androidx.compose
|
|
204
|
+
- **Android views** — import androidx.appcompat, or android.app.Activity
|
|
201
205
|
- **Supabase** — recorded alongside the engine it is — Postgres
|
|
202
206
|
- **Firebase** — Firestore
|
|
203
207
|
- **PlanetScale** — MySQL
|
|
@@ -189,6 +189,22 @@ function isTestOrExamplePath(file) {
|
|
|
189
189
|
* band — against a line written to be fake.
|
|
190
190
|
*/
|
|
191
191
|
|| /[-_]tests?\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file)
|
|
192
|
+
/**
|
|
193
|
+
* And the third spelling of it.
|
|
194
|
+
*
|
|
195
|
+
* netbox keeps `netbox/netbox/configuration_testing.py`, whose first three lines
|
|
196
|
+
* say it is a base configuration for testing and not intended for production. Its
|
|
197
|
+
* `SECRET_KEY = 'abcdefg...'` was the single `critical` in netbox's report — the
|
|
198
|
+
* severity that bars a report from the top band — raised against a line written to
|
|
199
|
+
* be thrown away.
|
|
200
|
+
*
|
|
201
|
+
* A directory called `testing/` has counted as tests here for a while, so the
|
|
202
|
+
* suffix is the same decision in the same ecosystem. It does cost a product file
|
|
203
|
+
* genuinely named `ab_testing.py`, which would go unread rather than be read
|
|
204
|
+
* wrongly — the cheaper of the two errors, and the same trade the `testing/`
|
|
205
|
+
* directory rule already makes.
|
|
206
|
+
*/
|
|
207
|
+
|| /[-_]testing\.py$/i.test(file)
|
|
192
208
|
|| /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file)
|
|
193
209
|
/**
|
|
194
210
|
* Sample code, which is what a documentation repository is made of.
|
|
@@ -247,6 +247,10 @@ const LABELS = {
|
|
|
247
247
|
'react-native': 'React Native',
|
|
248
248
|
ios: 'iOS (native)',
|
|
249
249
|
android: 'Android (native)',
|
|
250
|
+
swiftui: 'SwiftUI',
|
|
251
|
+
uikit: 'UIKit',
|
|
252
|
+
'jetpack compose': 'Jetpack Compose',
|
|
253
|
+
'android views': 'Android views',
|
|
250
254
|
express: 'Express',
|
|
251
255
|
flask: 'Flask',
|
|
252
256
|
fastapi: 'FastAPI',
|
|
@@ -370,6 +374,10 @@ function supportedStacks() {
|
|
|
370
374
|
label: 'Android (native)',
|
|
371
375
|
detectedFrom: 'AndroidManifest.xml, or build.gradle in either dialect',
|
|
372
376
|
},
|
|
377
|
+
{ id: 'swiftui', label: 'SwiftUI', detectedFrom: 'import SwiftUI' },
|
|
378
|
+
{ id: 'uikit', label: 'UIKit', detectedFrom: 'import UIKit' },
|
|
379
|
+
{ id: 'jetpack compose', label: 'Jetpack Compose', detectedFrom: 'import androidx.compose' },
|
|
380
|
+
{ id: 'android views', label: 'Android views', detectedFrom: 'import androidx.appcompat, or android.app.Activity' },
|
|
373
381
|
],
|
|
374
382
|
databases: entries(['postgres', 'mysql', 'sqlite', 'sqlserver', 'mongodb', 'redis', 'firestore', 'dynamodb', 'convex']),
|
|
375
383
|
dataPlatforms: [
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.detectFrontend = detectFrontend;
|
|
4
4
|
const detectContext_1 = require("./detectContext");
|
|
5
5
|
const catalogue_1 = require("./catalogue");
|
|
6
|
+
const textSearch_1 = require("../utils/textSearch");
|
|
6
7
|
async function detectFrontend(ctx) {
|
|
7
8
|
const frameworks = [];
|
|
8
9
|
const evidence = [];
|
|
@@ -34,6 +35,34 @@ async function detectFrontend(ctx) {
|
|
|
34
35
|
frameworks.push('flutter');
|
|
35
36
|
evidence.push({ type: 'dependency', value: 'flutter' });
|
|
36
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* The user interface a native application actually has.
|
|
40
|
+
*
|
|
41
|
+
* Nothing here could name one, so the question fell through to the fallback below
|
|
42
|
+
* and DuckDuckGo iOS — 1194 Swift files — was reported as "frontend: html", from the
|
|
43
|
+
* error pages and onboarding documents it ships inside the app. A reader sees that
|
|
44
|
+
* line and thinks web application.
|
|
45
|
+
*
|
|
46
|
+
* `import SwiftUI` and `import androidx.compose.runtime` are module imports the
|
|
47
|
+
* platform defines; an author who wants the toolkit has no other way to write it.
|
|
48
|
+
* The frameworks are mutually compatible in practice — an application migrating to
|
|
49
|
+
* SwiftUI still imports UIKit — so all of them that appear are recorded.
|
|
50
|
+
*/
|
|
51
|
+
const NATIVE_TOOLKITS = [
|
|
52
|
+
['swiftui', /^\s*import\s+SwiftUI\b/m],
|
|
53
|
+
['uikit', /^\s*import\s+UIKit\b/m],
|
|
54
|
+
['jetpack compose', /^\s*import\s+androidx\.compose\b/m],
|
|
55
|
+
['android views', /^\s*import\s+androidx\.appcompat\b|^\s*import\s+android\.app\.Activity\b/m],
|
|
56
|
+
];
|
|
57
|
+
for (const [toolkit, pattern] of NATIVE_TOOLKITS) {
|
|
58
|
+
const hits = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [pattern], 2);
|
|
59
|
+
if (hits.length === 0)
|
|
60
|
+
continue;
|
|
61
|
+
frameworks.push(toolkit);
|
|
62
|
+
for (const hit of hits.slice(0, 1)) {
|
|
63
|
+
evidence.push({ type: 'snippet', value: hit.snippet, file: hit.file, line: hit.line });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
37
66
|
/**
|
|
38
67
|
* A page is a front end, whether or not a framework built it.
|
|
39
68
|
*
|
|
@@ -28,6 +28,21 @@ const WILDCARD_ORIGIN = /(Access-Control-Allow-Origin["'\s:,]+\*)|(["']\*["'])/i
|
|
|
28
28
|
*/
|
|
29
29
|
const IMPORTS_FLASK_CORS = /from\s+flask_cors\s+import|import\s+flask_cors/;
|
|
30
30
|
const FLASK_CORS_CALL = /\bCORS\s*\(/;
|
|
31
|
+
/**
|
|
32
|
+
* Starlette's, which is how every FastAPI application does it.
|
|
33
|
+
*
|
|
34
|
+
* The full-stack FastAPI template — the one the framework's own organisation
|
|
35
|
+
* publishes — was told at `high` to add cross-origin handling, above
|
|
36
|
+
* `app.add_middleware(CORSMiddleware, allow_origins=[settings.FRONTEND_HOST])`. The
|
|
37
|
+
* class comes from the framework and `add_middleware` is its contract; neither is a
|
|
38
|
+
* word the author picked.
|
|
39
|
+
*
|
|
40
|
+
* The allowlist decides which case it is, read from the same call: `allow_origins`
|
|
41
|
+
* holding a bare `"*"` is the wide-open one, and anything else is a list somebody
|
|
42
|
+
* chose.
|
|
43
|
+
*/
|
|
44
|
+
const IMPORTS_STARLETTE_CORS = /from\s+(?:starlette|fastapi)\.middleware(?:\.cors)?\s+import[^\n]*CORSMiddleware/;
|
|
45
|
+
const STARLETTE_CORS_CALL = /add_middleware\s*\(\s*\n?\s*CORSMiddleware/;
|
|
31
46
|
/**
|
|
32
47
|
* The line with its quoted text removed.
|
|
33
48
|
*
|
|
@@ -298,6 +313,21 @@ async function detectSecurity(ctx) {
|
|
|
298
313
|
corsLoose.push(m);
|
|
299
314
|
}
|
|
300
315
|
}
|
|
316
|
+
if (IMPORTS_STARLETTE_CORS.test(text)) {
|
|
317
|
+
const lines = text.split(/\r?\n/);
|
|
318
|
+
const at = lines.findIndex((line) => /add_middleware\s*\(/.test(line) && (0, textSearch_1.isCitableLine)(line));
|
|
319
|
+
if (at !== -1) {
|
|
320
|
+
const window = lines.slice(at, Math.min(lines.length, at + 12)).join('\n');
|
|
321
|
+
if (STARLETTE_CORS_CALL.test(window)) {
|
|
322
|
+
const allowList = /allow_origins\s*=\s*\[([^\]]*)\]/.exec(window);
|
|
323
|
+
const hit = { file, line: at + 1, snippet: lines[at].trim().slice(0, 200) };
|
|
324
|
+
if (!allowList || /^\s*["']\*["']\s*,?\s*$/.test(allowList[1]))
|
|
325
|
+
corsLoose.push(hit);
|
|
326
|
+
else
|
|
327
|
+
corsStrict.push(hit);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
301
331
|
if (!IMPORTS_CORS.test(text))
|
|
302
332
|
continue;
|
|
303
333
|
const boundNames = new Set((corsBindings ?? []).filter((use) => use.file === file).map((use) => use.name));
|
|
@@ -308,6 +338,41 @@ async function detectSecurity(ctx) {
|
|
|
308
338
|
corsLoose.push(...detected.loose);
|
|
309
339
|
corsStrict.push(...detected.strict);
|
|
310
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Django's answer, which is a string in a list.
|
|
343
|
+
*
|
|
344
|
+
* netbox installs `corsheaders.middleware.CorsMiddleware` and was reported as having
|
|
345
|
+
* no cross-origin configuration at all — then told, at `high`, to add the handling it
|
|
346
|
+
* has. Nothing here looked for it: the reading was built around a call expression,
|
|
347
|
+
* and django-cors-headers is applied by naming it in `MIDDLEWARE` and configured by
|
|
348
|
+
* setting names the package itself defines.
|
|
349
|
+
*
|
|
350
|
+
* The package name and its setting names are the anchor, and neither is the author's
|
|
351
|
+
* to choose. Installed with no allowlist the package denies every cross-origin
|
|
352
|
+
* request, so the middleware alone is the strict case; `CORS_ALLOW_ALL_ORIGINS` — and
|
|
353
|
+
* `CORS_ORIGIN_ALLOW_ALL`, which is what it was called before version 3.5 — is the
|
|
354
|
+
* one line that opens it.
|
|
355
|
+
*/
|
|
356
|
+
const django = await findDjangoSettings(ctx);
|
|
357
|
+
if (django) {
|
|
358
|
+
const middlewareLine = django.text
|
|
359
|
+
.split(/\r?\n/)
|
|
360
|
+
.findIndex((line) => /corsheaders\.middleware\.CorsMiddleware/.test(line));
|
|
361
|
+
if (middlewareLine !== -1) {
|
|
362
|
+
const openedUp = /CORS_(?:ALLOW_ALL_ORIGINS|ORIGIN_ALLOW_ALL)\s*=\s*True/.exec(django.text);
|
|
363
|
+
const hit = {
|
|
364
|
+
file: django.file,
|
|
365
|
+
line: middlewareLine + 1,
|
|
366
|
+
snippet: openedUp
|
|
367
|
+
? openedUp[0]
|
|
368
|
+
: django.text.split(/\r?\n/)[middlewareLine].trim().slice(0, 200),
|
|
369
|
+
};
|
|
370
|
+
if (openedUp)
|
|
371
|
+
corsLoose.push(hit);
|
|
372
|
+
else
|
|
373
|
+
corsStrict.push(hit);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
311
376
|
for (const m of corsLoose)
|
|
312
377
|
evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'cors' });
|
|
313
378
|
for (const m of corsStrict)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.80.0",
|
|
4
4
|
"description": "Deterministic CLI and library that analyzes a web application repository and reports how far it is from production-ready for the kind of product it is meant to be.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|