@produtype/core 0.29.1 → 0.30.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.
|
@@ -37,7 +37,21 @@ const textSearch_1 = require("../utils/textSearch");
|
|
|
37
37
|
*/
|
|
38
38
|
/** Files that say which platform this is, without reading their contents. */
|
|
39
39
|
const IOS_MARKERS = [/(^|\/)Info\.plist$/i, /(^|\/)Podfile$/, /\.xcodeproj\//, /(^|\/)Package\.swift$/];
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* `AndroidManifest.xml` says Android. `build.gradle` says the JVM.
|
|
42
|
+
*
|
|
43
|
+
* Gradle builds Android applications, Spring services, Kotlin libraries and most of the
|
|
44
|
+
* Java world. Treating its presence as a platform made spring-petclinic — the canonical
|
|
45
|
+
* Spring web application — a mobile app at high confidence, and it would do the same to
|
|
46
|
+
* every JVM server ever written.
|
|
47
|
+
*
|
|
48
|
+
* A Gradle file earns the label by applying the Android plugin, which is the line that
|
|
49
|
+
* makes a build an Android build. That has to be read rather than matched on a path,
|
|
50
|
+
* which is why the two lists are separate.
|
|
51
|
+
*/
|
|
52
|
+
const ANDROID_MARKERS = [/(^|\/)AndroidManifest\.xml$/i];
|
|
53
|
+
const GRADLE_FILES = /(^|\/)build\.gradle(\.kts)?$/;
|
|
54
|
+
const ANDROID_GRADLE_PLUGIN = /com\.android\.(application|library)|(^|\s)android\s*\{/m;
|
|
41
55
|
/** Gradle coordinates that put a secret in the Android keystore rather than in a file. */
|
|
42
56
|
const SECURE_STORAGE_GRADLE = ['androidx.security:security-crypto', 'com.scottyab:secure-preferences'];
|
|
43
57
|
/**
|
|
@@ -110,9 +124,75 @@ function matchesAny(files, patterns) {
|
|
|
110
124
|
.filter((file) => !GENERATED_OR_VENDORED.test(file))
|
|
111
125
|
.filter((file) => patterns.some((pattern) => pattern.test(file)));
|
|
112
126
|
}
|
|
127
|
+
async function androidGradleFiles(ctx) {
|
|
128
|
+
const found = [];
|
|
129
|
+
for (const file of ctx.files.all.filter((candidate) => GRADLE_FILES.test(candidate))) {
|
|
130
|
+
const text = await (0, readTextFileSafe_1.readTextFileSafe)(ctx.root, file);
|
|
131
|
+
if (text && ANDROID_GRADLE_PLUGIN.test(text))
|
|
132
|
+
found.push(file);
|
|
133
|
+
}
|
|
134
|
+
return found;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Directory names that are part of a platform's layout rather than the project's.
|
|
138
|
+
*
|
|
139
|
+
* `src/ClientApp/Platforms/Android/AndroidManifest.xml` belongs to a project called
|
|
140
|
+
* ClientApp; the three segments after it are how MAUI arranges a project, not where the
|
|
141
|
+
* project begins. Walking past them finds the root a person would name.
|
|
142
|
+
*/
|
|
143
|
+
const PLATFORM_LAYOUT = new Set([
|
|
144
|
+
'platforms', 'android', 'ios', 'maccatalyst', 'app', 'src', 'main', 'res', 'xcshareddata',
|
|
145
|
+
]);
|
|
146
|
+
function mobileProjectRoot(marker) {
|
|
147
|
+
const segments = marker.split('/').slice(0, -1);
|
|
148
|
+
/**
|
|
149
|
+
* `.xcodeproj` sits beside the sources, not above them.
|
|
150
|
+
*
|
|
151
|
+
* `DonGeremIA.xcodeproj/project.pbxproj` names a project whose Swift files are in
|
|
152
|
+
* `DonGeremIA/`, a sibling. Stopping at the bundle put the project root somewhere no
|
|
153
|
+
* source file lives and scored a Swift application at zero.
|
|
154
|
+
*/
|
|
155
|
+
while (segments.length > 0
|
|
156
|
+
&& (PLATFORM_LAYOUT.has(segments[segments.length - 1].toLowerCase())
|
|
157
|
+
|| /\.(xcodeproj|xcworkspace)$/i.test(segments[segments.length - 1]))) {
|
|
158
|
+
segments.pop();
|
|
159
|
+
}
|
|
160
|
+
return segments.join('/');
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* How much of the repository the mobile project actually is.
|
|
164
|
+
*
|
|
165
|
+
* dotnet/eShop contains a real MAUI client — the markers are not a false positive — and
|
|
166
|
+
* it is one project of a dozen: 137 source files of 515, beside a web application and
|
|
167
|
+
* eight services. The repository was reported as a mobile application because a true
|
|
168
|
+
* signal about a part was read as a fact about the whole.
|
|
169
|
+
*
|
|
170
|
+
* Every repository in the verification corpus that *is* a phone application scores 1
|
|
171
|
+
* here: the manifest sits at its root. The gap between that and eShop's 0.27 is where
|
|
172
|
+
* the line goes.
|
|
173
|
+
*/
|
|
174
|
+
function mobileSourceShare(sourceFiles, markers) {
|
|
175
|
+
if (sourceFiles.length === 0)
|
|
176
|
+
return 0;
|
|
177
|
+
/**
|
|
178
|
+
* No file marker means the platform came from a dependency.
|
|
179
|
+
*
|
|
180
|
+
* A Flutter project is recognised by `flutter` in its `pubspec.yaml`, and a React
|
|
181
|
+
* Native one by its `package.json` — a fact about the project's own manifest rather
|
|
182
|
+
* than about a subdirectory. Scoring those at zero made every Flutter application in
|
|
183
|
+
* the corpus stop being a mobile application.
|
|
184
|
+
*/
|
|
185
|
+
if (markers.length === 0)
|
|
186
|
+
return 1;
|
|
187
|
+
const roots = new Set(markers.map(mobileProjectRoot));
|
|
188
|
+
if (roots.has(''))
|
|
189
|
+
return 1;
|
|
190
|
+
const inside = sourceFiles.filter((file) => [...roots].some((root) => file.startsWith(`${root}/`)));
|
|
191
|
+
return inside.length / sourceFiles.length;
|
|
192
|
+
}
|
|
113
193
|
async function detectMobile(ctx) {
|
|
114
194
|
const iosFiles = matchesAny(ctx.files.all, IOS_MARKERS);
|
|
115
|
-
const androidFiles = matchesAny(ctx.files.all, ANDROID_MARKERS);
|
|
195
|
+
const androidFiles = [...matchesAny(ctx.files.all, ANDROID_MARKERS), ...(await androidGradleFiles(ctx))];
|
|
116
196
|
const flutterDeps = (0, detectContext_1.hasAnyDartDep)(ctx, ['flutter']);
|
|
117
197
|
const reactNativeDeps = (0, detectContext_1.hasAnyDep)(ctx, ['react-native', 'expo']);
|
|
118
198
|
const platforms = [];
|
|
@@ -135,6 +215,7 @@ async function detectMobile(ctx) {
|
|
|
135
215
|
platformEvidence.push({ type: 'file', value: androidFiles[0], file: androidFiles[0] });
|
|
136
216
|
}
|
|
137
217
|
const isMobile = platforms.length > 0;
|
|
218
|
+
const share = mobileSourceShare(ctx.files.source, [...iosFiles, ...androidFiles]);
|
|
138
219
|
/**
|
|
139
220
|
* Every capability below reports `present: false` when this is not a mobile project,
|
|
140
221
|
* with no evidence — the profile that asks about them is the only one that applies
|
|
@@ -263,7 +344,7 @@ async function detectMobile(ctx) {
|
|
|
263
344
|
key: 'mobile.platform',
|
|
264
345
|
present: true,
|
|
265
346
|
evidence: platformEvidence,
|
|
266
|
-
details: { platforms },
|
|
347
|
+
details: { platforms, sourceShare: share },
|
|
267
348
|
},
|
|
268
349
|
{
|
|
269
350
|
key: 'mobile.permissions',
|
|
@@ -56,6 +56,14 @@ export interface ProfileFacts {
|
|
|
56
56
|
gameSignals: number;
|
|
57
57
|
/** Ships to a phone: Flutter, React Native, or an iOS/Android project in the tree. */
|
|
58
58
|
mobilePlatforms: string[];
|
|
59
|
+
/**
|
|
60
|
+
* How much of the repository the mobile project is, between 0 and 1.
|
|
61
|
+
*
|
|
62
|
+
* A true signal about one project was being read as a fact about the whole: eShop's
|
|
63
|
+
* MAUI client is 137 source files of 515, beside a web application and eight services,
|
|
64
|
+
* and the repository came back as a phone application.
|
|
65
|
+
*/
|
|
66
|
+
mobileShare: number;
|
|
59
67
|
sourceFiles: number;
|
|
60
68
|
}
|
|
61
69
|
export declare function readFacts(analysis: ProjectAnalysis): ProfileFacts;
|
|
@@ -33,6 +33,7 @@ function readFacts(analysis) {
|
|
|
33
33
|
tests: present('quality.tests'),
|
|
34
34
|
gameSignals: Number(analysis.detectors['game.engine']?.details?.supportingSignals ?? 0),
|
|
35
35
|
mobilePlatforms: analysis.detectors['mobile.platform']?.details?.platforms ?? [],
|
|
36
|
+
mobileShare: analysis.detectors['mobile.platform']?.details?.sourceShare ?? 0,
|
|
36
37
|
sourceFiles: analysis.files.source.length,
|
|
37
38
|
};
|
|
38
39
|
}
|
|
@@ -134,7 +135,15 @@ const RULES = [
|
|
|
134
135
|
*/
|
|
135
136
|
profile: 'mobile-app',
|
|
136
137
|
refines: 'client-app',
|
|
137
|
-
|
|
138
|
+
/**
|
|
139
|
+
* The mobile project has to be the repository, not a project inside it.
|
|
140
|
+
*
|
|
141
|
+
* Every repository in the corpus that is a phone application has its manifest at the
|
|
142
|
+
* root and scores 1. eShop scores 0.27 and is a .NET system with a MAUI client in it.
|
|
143
|
+
* Half is the line, and it sits in the gap between those two rather than in the
|
|
144
|
+
* middle of a distribution.
|
|
145
|
+
*/
|
|
146
|
+
admissible: (f) => f.mobilePlatforms.length > 0 && f.mobileShare >= 0.5 && !f.tenancy,
|
|
138
147
|
signals: [
|
|
139
148
|
{ identifies: true, label: 'a mobile project in the repository', weight: 5, holds: (f) => f.mobilePlatforms.length > 0 },
|
|
140
149
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.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": {
|