@produtype/core 0.77.0 → 0.79.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 +5 -1
- package/dist/analyzer/catalogue.js +8 -0
- package/dist/analyzer/detectBackend.js +4 -27
- package/dist/analyzer/detectFrontend.js +29 -0
- package/dist/analyzer/detectPackageManager.js +77 -11
- package/dist/analyzer/languageShare.d.ts +20 -0
- package/dist/analyzer/languageShare.js +28 -0
- package/dist/analyzer/types.d.ts +1 -1
- package/package.json +1 -1
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
|
|
@@ -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: [
|
|
@@ -4,31 +4,8 @@ exports.detectBackend = detectBackend;
|
|
|
4
4
|
const detectContext_1 = require("./detectContext");
|
|
5
5
|
const textSearch_1 = require("../utils/textSearch");
|
|
6
6
|
const readTextFileSafe_1 = require("../utils/readTextFileSafe");
|
|
7
|
+
const languageShare_1 = require("./languageShare");
|
|
7
8
|
const catalogue_1 = require("./catalogue");
|
|
8
|
-
/**
|
|
9
|
-
* How much of the source is written in one language.
|
|
10
|
-
*
|
|
11
|
-
* A manifest says a language is present; a share says it is what the product is made
|
|
12
|
-
* of. The mobile detector has drawn this distinction since a single MAUI client
|
|
13
|
-
* decided the profile of a nine-project .NET solution.
|
|
14
|
-
*/
|
|
15
|
-
function languageShare(ctx, extension) {
|
|
16
|
-
const source = ctx.files.source;
|
|
17
|
-
if (source.length === 0)
|
|
18
|
-
return 0;
|
|
19
|
-
return source.filter((file) => extension.test(file)).length / source.length;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Below this a language is present in the repository without being what serves the
|
|
23
|
-
* requests.
|
|
24
|
-
*
|
|
25
|
-
* windmill is the measurement: two Go files beside 547 Rust ones, 0.05% of its source,
|
|
26
|
-
* and the report said "Backend: go". The line is not tuned to that case — anything
|
|
27
|
-
* under one file in twenty is a client, a script or a sample, and every backend in the
|
|
28
|
-
* verification corpus is far above it. A repository genuinely split between two server
|
|
29
|
-
* languages reports both, which is the right answer for one.
|
|
30
|
-
*/
|
|
31
|
-
const MINIMUM_BACKEND_SHARE = 0.05;
|
|
32
9
|
async function detectBackend(ctx) {
|
|
33
10
|
const frameworks = [];
|
|
34
11
|
const evidence = [];
|
|
@@ -94,8 +71,8 @@ async function detectBackend(ctx) {
|
|
|
94
71
|
* "Backend: go". The same share test the mobile detector already uses: a language
|
|
95
72
|
* has to be a real part of what is written here before it names the backend.
|
|
96
73
|
*/
|
|
97
|
-
const goShare = languageShare(ctx, /\.go$/);
|
|
98
|
-
if (!namedGoFramework && goShare >=
|
|
74
|
+
const goShare = (0, languageShare_1.languageShare)(ctx, /\.go$/);
|
|
75
|
+
if (!namedGoFramework && goShare >= languageShare_1.MINIMUM_LANGUAGE_SHARE && ctx.files.all.some((f) => /(^|\/)go\.mod$/.test(f))) {
|
|
99
76
|
frameworks.push('go');
|
|
100
77
|
evidence.push({ type: 'note', value: 'a Go module with no web framework named in go.mod' });
|
|
101
78
|
}
|
|
@@ -157,7 +134,7 @@ async function detectBackend(ctx) {
|
|
|
157
134
|
* before it names the backend.
|
|
158
135
|
*/
|
|
159
136
|
if (!namedRubyFramework
|
|
160
|
-
&& languageShare(ctx, /\.rb$/) >=
|
|
137
|
+
&& (0, languageShare_1.languageShare)(ctx, /\.rb$/) >= languageShare_1.MINIMUM_LANGUAGE_SHARE
|
|
161
138
|
&& ctx.files.all.some((f) => /(^|\/)Gemfile$/.test(f))) {
|
|
162
139
|
frameworks.push('ruby');
|
|
163
140
|
evidence.push({ type: 'note', value: 'a Gemfile with no web framework in it' });
|
|
@@ -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
|
*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.detectPackageManager = detectPackageManager;
|
|
4
|
+
const languageShare_1 = require("./languageShare");
|
|
4
5
|
/**
|
|
5
6
|
* Manifests outside Node and Python, each one a file this analyzer already parses.
|
|
6
7
|
*
|
|
@@ -20,6 +21,15 @@ const OTHER_MANIFESTS = [
|
|
|
20
21
|
{ manager: 'gradle', pattern: /(^|\/)build\.gradle(\.kts)?$/ },
|
|
21
22
|
{ manager: 'maven', pattern: /(^|\/)pom\.xml$/ },
|
|
22
23
|
{ manager: 'nuget', pattern: /\.(csproj|fsproj|vbproj)$/i },
|
|
24
|
+
/**
|
|
25
|
+
* Both Apple ecosystems, added once their manifests were actually being parsed —
|
|
26
|
+
* `Package.swift` and the Podfile for a long while, `Package.resolved` since 0.75.0.
|
|
27
|
+
* Until then an iOS application could only ever come out as `npm` or `unknown`,
|
|
28
|
+
* which is how DuckDuckGo iOS — 1194 Swift files — reported "npm (lockfile)" from a
|
|
29
|
+
* package.json whose only job is running rollup over one content-blocking script.
|
|
30
|
+
*/
|
|
31
|
+
{ manager: 'swift package manager', pattern: /(^|\/)Package\.(swift|resolved)$/ },
|
|
32
|
+
{ manager: 'cocoapods', pattern: /(^|\/)Podfile$/ },
|
|
23
33
|
];
|
|
24
34
|
/**
|
|
25
35
|
* The shallowest manifest wins, and list order only breaks a tie.
|
|
@@ -33,22 +43,59 @@ const OTHER_MANIFESTS = [
|
|
|
33
43
|
* the project's; one buried under `sdk/go/` belongs to something the project ships
|
|
34
44
|
* rather than something it is built with.
|
|
35
45
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The language each of these manifests builds, so a share can be asked about it.
|
|
48
|
+
*
|
|
49
|
+
* Depth alone decided before, and depth is the wrong question when the shallow
|
|
50
|
+
* manifest belongs to the build tooling: DuckDuckGo iOS keeps a fastlane `Gemfile` at
|
|
51
|
+
* its root and its real Swift manifest five directories down inside the `.xcodeproj`,
|
|
52
|
+
* so the report called a 1194-file iOS application a Ruby project — the same mistake
|
|
53
|
+
* WordPress-iOS produced one layer up, where the fastlane Gemfile was read as the
|
|
54
|
+
* backend.
|
|
55
|
+
*/
|
|
56
|
+
const MANIFEST_LANGUAGE = {
|
|
57
|
+
pub: /\.dart$/,
|
|
58
|
+
composer: /\.php$/,
|
|
59
|
+
'go modules': /\.go$/,
|
|
60
|
+
cargo: /\.rs$/,
|
|
61
|
+
bundler: /\.rb$/,
|
|
62
|
+
gradle: /\.(java|kt|kts|scala|groovy)$/,
|
|
63
|
+
maven: /\.(java|kt|kts|scala|groovy)$/,
|
|
64
|
+
nuget: /\.(cs|fs|vb)$/,
|
|
65
|
+
'swift package manager': /\.(swift|m|mm)$/,
|
|
66
|
+
cocoapods: /\.(swift|m|mm)$/,
|
|
67
|
+
};
|
|
68
|
+
function resolveFromOtherManifests(ctx) {
|
|
69
|
+
const files = ctx.files.all;
|
|
70
|
+
const candidates = [];
|
|
38
71
|
OTHER_MANIFESTS.forEach(({ manager, pattern }, order) => {
|
|
39
72
|
for (const file of files) {
|
|
40
73
|
if (!pattern.test(file))
|
|
41
74
|
continue;
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
75
|
+
const extension = MANIFEST_LANGUAGE[manager];
|
|
76
|
+
candidates.push({
|
|
77
|
+
manager,
|
|
78
|
+
file,
|
|
79
|
+
depth: file.split('/').length,
|
|
80
|
+
order,
|
|
81
|
+
share: extension ? (0, languageShare_1.languageShare)(ctx, extension) : 0,
|
|
82
|
+
});
|
|
46
83
|
}
|
|
47
84
|
});
|
|
48
|
-
if (
|
|
85
|
+
if (candidates.length === 0)
|
|
49
86
|
return null;
|
|
50
|
-
|
|
51
|
-
|
|
87
|
+
/**
|
|
88
|
+
* A manifest whose language is really here outranks one that is merely nearer the
|
|
89
|
+
* root. Where none of them clears the line — a repository of configuration, or one
|
|
90
|
+
* whose language this analyzer does not count — the old ordering stands, so nothing
|
|
91
|
+
* that used to resolve stops resolving.
|
|
92
|
+
*/
|
|
93
|
+
const substantial = candidates.filter((candidate) => candidate.share >= languageShare_1.MINIMUM_LANGUAGE_SHARE);
|
|
94
|
+
const pool = substantial.length > 0 ? substantial : candidates;
|
|
95
|
+
const best = pool.reduce((winner, candidate) => candidate.depth < winner.depth || (candidate.depth === winner.depth && candidate.order < winner.order)
|
|
96
|
+
? candidate
|
|
97
|
+
: winner);
|
|
98
|
+
return { manager: best.manager, confidence: 'manifest', warnings: [], evidence: [best.file] };
|
|
52
99
|
}
|
|
53
100
|
function resolveWorkspaceManager(workspace) {
|
|
54
101
|
const warnings = [];
|
|
@@ -91,9 +138,28 @@ async function detectPackageManager(ctx) {
|
|
|
91
138
|
const fromWorkspaces = rootWorkspace ?? workspaceManagers.find((w) => w.manager !== 'unknown');
|
|
92
139
|
// Node and Python first, because the workspace record is built from their manifests and
|
|
93
140
|
// knows which of several it found. Anything else is resolved from the file list.
|
|
94
|
-
const
|
|
141
|
+
const otherManifest = resolveFromOtherManifests(ctx);
|
|
142
|
+
/**
|
|
143
|
+
* A package.json is not always what the project is built with.
|
|
144
|
+
*
|
|
145
|
+
* It wins over every other manifest here because the workspace record is richer, and
|
|
146
|
+
* that was right until it met a repository where Node is the build tooling rather
|
|
147
|
+
* than the product: DuckDuckGo iOS keeps one to run rollup over a single
|
|
148
|
+
* content-blocking script, eight JavaScript files against 1194 Swift ones, and the
|
|
149
|
+
* report said "Package manager: npm (lockfile)".
|
|
150
|
+
*
|
|
151
|
+
* The same share test the backend uses, and for the same reason — one file in twenty
|
|
152
|
+
* is the line below which a language is something the repository contains rather
|
|
153
|
+
* than something it is made of. Only applied when there is another manifest to
|
|
154
|
+
* prefer: a repository whose only manifest is a package.json is an npm repository
|
|
155
|
+
* however little JavaScript it has.
|
|
156
|
+
*/
|
|
157
|
+
const nodeOrPythonIsTheProduct = (0, languageShare_1.languageShare)(ctx, /\.(ts|tsx|js|jsx|mjs|cjs|py)$/) >= languageShare_1.MINIMUM_LANGUAGE_SHARE;
|
|
158
|
+
const selected = fromWorkspaces
|
|
159
|
+
&& fromWorkspaces.manager !== 'unknown'
|
|
160
|
+
&& (nodeOrPythonIsTheProduct || !otherManifest)
|
|
95
161
|
? fromWorkspaces
|
|
96
|
-
:
|
|
162
|
+
: otherManifest ?? fromWorkspaces;
|
|
97
163
|
if (selected) {
|
|
98
164
|
return {
|
|
99
165
|
manager: selected.manager,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DetectContext } from './detectContext';
|
|
2
|
+
/**
|
|
3
|
+
* How much of a repository's source is written in a given language.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from the backend detector, which grew it first: two Go files beside 547
|
|
6
|
+
* Rust ones made a report say "Backend: go". The same question decides more than one
|
|
7
|
+
* claim — which language serves the requests, and which ecosystem builds the project —
|
|
8
|
+
* and the answer should be arrived at the same way in both.
|
|
9
|
+
*/
|
|
10
|
+
export declare function languageShare(ctx: DetectContext, extension: RegExp): number;
|
|
11
|
+
/**
|
|
12
|
+
* Below this a language is present in the repository without being what it is made of.
|
|
13
|
+
*
|
|
14
|
+
* windmill is the measurement: two Go files beside 547 Rust ones, 0.05% of its source,
|
|
15
|
+
* and the report said "Backend: go". The line is not tuned to that case — anything
|
|
16
|
+
* under one file in twenty is a client, a script or a sample, and every backend in the
|
|
17
|
+
* verification corpus is far above it. A repository genuinely split between two
|
|
18
|
+
* languages reports both, which is the right answer for one.
|
|
19
|
+
*/
|
|
20
|
+
export declare const MINIMUM_LANGUAGE_SHARE = 0.05;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MINIMUM_LANGUAGE_SHARE = void 0;
|
|
4
|
+
exports.languageShare = languageShare;
|
|
5
|
+
/**
|
|
6
|
+
* How much of a repository's source is written in a given language.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from the backend detector, which grew it first: two Go files beside 547
|
|
9
|
+
* Rust ones made a report say "Backend: go". The same question decides more than one
|
|
10
|
+
* claim — which language serves the requests, and which ecosystem builds the project —
|
|
11
|
+
* and the answer should be arrived at the same way in both.
|
|
12
|
+
*/
|
|
13
|
+
function languageShare(ctx, extension) {
|
|
14
|
+
const source = ctx.files.source;
|
|
15
|
+
if (source.length === 0)
|
|
16
|
+
return 0;
|
|
17
|
+
return source.filter((file) => extension.test(file)).length / source.length;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Below this a language is present in the repository without being what it is made of.
|
|
21
|
+
*
|
|
22
|
+
* windmill is the measurement: two Go files beside 547 Rust ones, 0.05% of its source,
|
|
23
|
+
* and the report said "Backend: go". The line is not tuned to that case — anything
|
|
24
|
+
* under one file in twenty is a client, a script or a sample, and every backend in the
|
|
25
|
+
* verification corpus is far above it. A repository genuinely split between two
|
|
26
|
+
* languages reports both, which is the right answer for one.
|
|
27
|
+
*/
|
|
28
|
+
exports.MINIMUM_LANGUAGE_SHARE = 0.05;
|
package/dist/analyzer/types.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Only managers whose manifest this analyzer actually reads are named here. A name it
|
|
11
11
|
* cannot back with a parsed file would be a guess from a filename.
|
|
12
12
|
*/
|
|
13
|
-
export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'pip' | 'poetry' | 'python' | 'pub' | 'composer' | 'go modules' | 'cargo' | 'bundler' | 'gradle' | 'maven' | 'nuget' | 'unknown';
|
|
13
|
+
export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'pip' | 'poetry' | 'python' | 'pub' | 'composer' | 'go modules' | 'cargo' | 'bundler' | 'gradle' | 'maven' | 'nuget' | 'swift package manager' | 'cocoapods' | 'unknown';
|
|
14
14
|
export type PackageManagerConfidence = 'lockfile' | 'manifest' | 'inferred' | 'unknown';
|
|
15
15
|
export interface WorkspaceStack {
|
|
16
16
|
root: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.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": {
|