@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 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 >= MINIMUM_BACKEND_SHARE && ctx.files.all.some((f) => /(^|\/)go\.mod$/.test(f))) {
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$/) >= MINIMUM_BACKEND_SHARE
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
- function resolveFromOtherManifests(files) {
37
- let best = null;
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 depth = file.split('/').length;
43
- if (!best || depth < best.depth || (depth === best.depth && order < best.order)) {
44
- best = { manager, file, depth, order };
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 (!best)
85
+ if (candidates.length === 0)
49
86
  return null;
50
- const { manager, file } = best;
51
- return { manager, confidence: 'manifest', warnings: [], evidence: [file] };
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 selected = fromWorkspaces && fromWorkspaces.manager !== 'unknown'
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
- : resolveFromOtherManifests(ctx.files.all) ?? fromWorkspaces;
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;
@@ -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.77.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": {