@produtype/core 0.77.0 → 0.78.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.
|
@@ -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' });
|
|
@@ -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.78.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": {
|