@produtype/core 0.76.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 >= 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' });
@@ -354,6 +354,17 @@ async function detectMobile(ctx) {
354
354
  {
355
355
  key: 'mobile.privacyDeclaration',
356
356
  present: privacyFiles.length > 0,
357
+ /**
358
+ * On Android the declaration is not in the repository, and never was.
359
+ *
360
+ * Apple has required `PrivacyInfo.xcprivacy` in the bundle since 2024, so an iOS
361
+ * project either has the file or has not made the declaration. Play's data-safety
362
+ * form is filled in the console: there is no committed artifact to find, and no
363
+ * convention that puts one in the tree. thunderbird-android was told it was
364
+ * missing a file it has no way to have — a question this analyzer cannot ask
365
+ * rather than an answer of no.
366
+ */
367
+ unanswered: privacyFiles.length === 0 && !platforms.includes('ios'),
357
368
  evidence: (0, absenceEvidence_1.evidenceOrSearch)(privacyFiles.map((file) => ({ type: 'file', value: file, file })), 'the privacy declaration the stores require', ['PrivacyInfo.xcprivacy', 'a data-safety declaration', 'privacy_policy', 'PRIVACY.md']),
358
369
  },
359
370
  ];
@@ -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;
@@ -224,6 +224,16 @@ function deriveStatus(analysis, capability) {
224
224
  return 'present';
225
225
  if (matches.some((m) => m.present))
226
226
  return 'partial';
227
+ /**
228
+ * Nothing found, and at least one detector says it could not look.
229
+ *
230
+ * The specific rules in `rules.ts` grew this in 0.74.0 one claim at a time; this
231
+ * is the same rule for every capability that reaches the generic path. A finding
232
+ * is only ever weakened by it: `present` and `partial` are decided above, so
233
+ * blindness can turn `missing` into `unknown` and nothing else.
234
+ */
235
+ if (matches.some((m) => m.unanswered))
236
+ return 'unknown';
227
237
  return 'missing';
228
238
  }
229
239
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.76.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": {