@produtype/core 0.52.0 → 0.53.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.
|
@@ -188,7 +188,30 @@ function isTestOrExamplePath(file) {
|
|
|
188
188
|
* band — against a line written to be fake.
|
|
189
189
|
*/
|
|
190
190
|
|| /[-_]tests?\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file)
|
|
191
|
-
|| /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file)
|
|
191
|
+
|| /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file)
|
|
192
|
+
/**
|
|
193
|
+
* Sample code, which is what a documentation repository is made of.
|
|
194
|
+
*
|
|
195
|
+
* Every one of ktor-documentation's 435 Kotlin files sits under
|
|
196
|
+
* `codeSnippets/snippets/`, and the report judged them as a product: nine essential
|
|
197
|
+
* capabilities missing at `high`, including email verification and GDPR consent,
|
|
198
|
+
* for a repository that is documentation.
|
|
199
|
+
*
|
|
200
|
+
* `examples` was already here in the sense that mattered to Node; `samples` and
|
|
201
|
+
* `snippets` are the same idea spelled the way the JVM and the Rust ecosystems
|
|
202
|
+
* spell it. A repository whose every source file is a sample now has no source
|
|
203
|
+
* files, and says so — which is the honest answer for one.
|
|
204
|
+
*/
|
|
205
|
+
|| /(^|\/)(samples?|snippets?|codesnippets)(\/|$)/i.test(file)
|
|
206
|
+
/**
|
|
207
|
+
* Somebody else's code, vendored in.
|
|
208
|
+
*
|
|
209
|
+
* meilisearch was reported as using Rocket. Rocket appears once in the repository,
|
|
210
|
+
* as a dev-dependency of `external-crates/reqwest-eventsource` — a third-party
|
|
211
|
+
* crate copied in whole. A manifest under a vendor directory is a statement about
|
|
212
|
+
* that library, not about this product.
|
|
213
|
+
*/
|
|
214
|
+
|| /(^|\/)(vendor|vendored|third[-_]?party|external[-_]crates)(\/|$)/i.test(file);
|
|
192
215
|
}
|
|
193
216
|
/**
|
|
194
217
|
* Extensions the detectors can read.
|
|
@@ -367,6 +390,18 @@ async function analyzeProject(projectPath) {
|
|
|
367
390
|
const configFiles = pickConfig(allFiles);
|
|
368
391
|
const packageJsonRaw = await (0, readTextFileSafe_1.readJsonSafe)(root, 'package.json');
|
|
369
392
|
const packageJson = packageJsonRaw ? packageJsonSchema.parse(packageJsonRaw) : null;
|
|
393
|
+
/**
|
|
394
|
+
* The manifests this project owns, without the ones it merely contains.
|
|
395
|
+
*
|
|
396
|
+
* A vendored crate, a sample application and a test harness each carry a manifest,
|
|
397
|
+
* and each says something about itself rather than about the product. meilisearch
|
|
398
|
+
* was reported as using Rocket on the strength of a dev-dependency inside
|
|
399
|
+
* `external-crates/reqwest-eventsource` — a third-party library copied in whole.
|
|
400
|
+
*
|
|
401
|
+
* The same rule the source list has always used, applied to the file list the
|
|
402
|
+
* manifest readers walk.
|
|
403
|
+
*/
|
|
404
|
+
const ownManifests = allFiles.filter((f) => !isTestOrExamplePath(f));
|
|
370
405
|
const packageJsonFiles = allFiles.filter((f) => f.endsWith('package.json'));
|
|
371
406
|
const requirementsFiles = allFiles.filter((f) => /(^|\/)requirements\.txt$/i.test(f));
|
|
372
407
|
const pyprojectFiles = allFiles.filter((f) => /(^|\/)pyproject\.toml$/i.test(f));
|
|
@@ -427,7 +462,7 @@ async function analyzeProject(projectPath) {
|
|
|
427
462
|
* than a wrong answer.
|
|
428
463
|
*/
|
|
429
464
|
const goDeps = [];
|
|
430
|
-
for (const file of
|
|
465
|
+
for (const file of ownManifests.filter((f) => /(^|\/)go\.mod$/.test(f))) {
|
|
431
466
|
const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
|
|
432
467
|
for (const line of raw.split('\n')) {
|
|
433
468
|
const match = /^\s*(?:require\s+)?([a-z0-9][\w.-]*(?:\.[a-z]{2,})?\/[\w./-]+)\s+v/i.exec(line);
|
|
@@ -449,27 +484,44 @@ async function analyzeProject(projectPath) {
|
|
|
449
484
|
* read.
|
|
450
485
|
*/
|
|
451
486
|
const rustDeps = [];
|
|
452
|
-
|
|
487
|
+
/**
|
|
488
|
+
* The crates the product ships with, without the ones it only builds and tests with.
|
|
489
|
+
*
|
|
490
|
+
* meilisearch was reported as using Rocket, from a `[dev-dependencies]` block. The
|
|
491
|
+
* same distinction `runtimeNpmDeps` draws for the reason axios taught: a web
|
|
492
|
+
* framework in dev-dependencies is a test server, and the question "what serves the
|
|
493
|
+
* requests" is answered by what ships.
|
|
494
|
+
*/
|
|
495
|
+
const runtimeRustDeps = [];
|
|
496
|
+
for (const file of ownManifests.filter((f) => /(^|\/)Cargo\.toml$/.test(f))) {
|
|
453
497
|
const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
|
|
454
498
|
let inDeps = false;
|
|
499
|
+
let runtimeSection = false;
|
|
455
500
|
for (const line of raw.split('\n')) {
|
|
456
501
|
const heading = /^\s*\[([^\]]+)\]/.exec(line);
|
|
457
502
|
if (heading) {
|
|
458
503
|
const section = heading[1].trim();
|
|
459
504
|
const nested = /^(?:[a-z-]+\.)?(?:dependencies|dev-dependencies|build-dependencies)\.(.+)$/.exec(section);
|
|
460
505
|
if (nested) {
|
|
461
|
-
|
|
506
|
+
const crate = nested[1].trim().toLowerCase();
|
|
507
|
+
rustDeps.push(crate);
|
|
508
|
+
if (/(^|\.)dependencies\./.test(section))
|
|
509
|
+
runtimeRustDeps.push(crate);
|
|
462
510
|
inDeps = false;
|
|
463
511
|
continue;
|
|
464
512
|
}
|
|
465
513
|
inDeps = /(^|\.)(dependencies|dev-dependencies|build-dependencies)$/.test(section);
|
|
514
|
+
runtimeSection = /(^|\.)dependencies$/.test(section);
|
|
466
515
|
continue;
|
|
467
516
|
}
|
|
468
517
|
if (!inDeps)
|
|
469
518
|
continue;
|
|
470
519
|
const entry = /^\s*([A-Za-z0-9_-]+)\s*=/.exec(line);
|
|
471
|
-
if (entry)
|
|
520
|
+
if (entry) {
|
|
472
521
|
rustDeps.push(entry[1].toLowerCase());
|
|
522
|
+
if (runtimeSection)
|
|
523
|
+
runtimeRustDeps.push(entry[1].toLowerCase());
|
|
524
|
+
}
|
|
473
525
|
}
|
|
474
526
|
}
|
|
475
527
|
/**
|
|
@@ -482,7 +534,7 @@ async function analyzeProject(projectPath) {
|
|
|
482
534
|
* from.
|
|
483
535
|
*/
|
|
484
536
|
const dartDeps = [];
|
|
485
|
-
for (const file of
|
|
537
|
+
for (const file of ownManifests.filter((f) => /(^|\/)pubspec\.yaml$/.test(f))) {
|
|
486
538
|
const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
|
|
487
539
|
let inDeps = false;
|
|
488
540
|
for (const line of raw.split('\n')) {
|
|
@@ -543,13 +595,13 @@ async function analyzeProject(projectPath) {
|
|
|
543
595
|
* no dependencies rather than a wrong answer.
|
|
544
596
|
*/
|
|
545
597
|
const gradleDeps = [];
|
|
546
|
-
for (const file of
|
|
598
|
+
for (const file of ownManifests.filter((f) => /(^|\/)pom\.xml$/.test(f))) {
|
|
547
599
|
const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
|
|
548
600
|
for (const match of raw.matchAll(/<groupId>\s*([^<\s]+)\s*<\/groupId>\s*<artifactId>\s*([^<\s]+)\s*<\/artifactId>/g)) {
|
|
549
601
|
gradleDeps.push(`${match[1]}:${match[2]}`.toLowerCase());
|
|
550
602
|
}
|
|
551
603
|
}
|
|
552
|
-
for (const file of
|
|
604
|
+
for (const file of ownManifests.filter((f) => /(^|\/)build\.gradle(\.kts)?$/.test(f))) {
|
|
553
605
|
const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
|
|
554
606
|
for (const match of raw.matchAll(/\b(?:implementation|api|compileOnly|runtimeOnly|kapt|ksp|annotationProcessor|testImplementation)\s*[( ]\s*["']([^"']+)["']/g)) {
|
|
555
607
|
const [group, artifact] = match[1].split(':');
|
|
@@ -557,7 +609,7 @@ async function analyzeProject(projectPath) {
|
|
|
557
609
|
gradleDeps.push(`${group}:${artifact}`.toLowerCase());
|
|
558
610
|
}
|
|
559
611
|
}
|
|
560
|
-
for (const file of
|
|
612
|
+
for (const file of ownManifests.filter((f) => /(^|\/)libs\.versions\.toml$/.test(f))) {
|
|
561
613
|
const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
|
|
562
614
|
// Two spellings, both common: group and name as separate keys, or one `module`
|
|
563
615
|
// holding the coordinate. Order within the line varies, so each is matched on its
|
|
@@ -586,13 +638,13 @@ async function analyzeProject(projectPath) {
|
|
|
586
638
|
* CocoaPods is simpler and still in wide use: `pod 'Alamofire'`.
|
|
587
639
|
*/
|
|
588
640
|
const swiftDeps = [];
|
|
589
|
-
for (const file of
|
|
641
|
+
for (const file of ownManifests.filter((f) => /(^|\/)Package\.swift$/.test(f))) {
|
|
590
642
|
const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
|
|
591
643
|
for (const match of raw.matchAll(/\.package\s*\(\s*url:\s*["']https?:\/\/[^"']*?\/([^/"']+?)\/([^/"']+?)(?:\.git)?["']/g)) {
|
|
592
644
|
swiftDeps.push(`${match[1]}/${match[2]}`.toLowerCase());
|
|
593
645
|
}
|
|
594
646
|
}
|
|
595
|
-
for (const file of
|
|
647
|
+
for (const file of ownManifests.filter((f) => /(^|\/)Podfile$/.test(f))) {
|
|
596
648
|
const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
|
|
597
649
|
for (const match of raw.matchAll(/^\s*pod\s+["']([^"'/]+)/gm)) {
|
|
598
650
|
swiftDeps.push(match[1].toLowerCase());
|
|
@@ -613,7 +665,7 @@ async function analyzeProject(projectPath) {
|
|
|
613
665
|
*/
|
|
614
666
|
const dotnetDeps = [];
|
|
615
667
|
let dotnetWebSdk = false;
|
|
616
|
-
for (const file of
|
|
668
|
+
for (const file of ownManifests.filter((f) => /\.(csproj|fsproj|vbproj)$/i.test(f))) {
|
|
617
669
|
const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
|
|
618
670
|
if (/Sdk\s*=\s*["']Microsoft\.NET\.Sdk\.Web["']/i.test(raw))
|
|
619
671
|
dotnetWebSdk = true;
|
|
@@ -626,7 +678,7 @@ async function analyzeProject(projectPath) {
|
|
|
626
678
|
}
|
|
627
679
|
}
|
|
628
680
|
const rubyDeps = [];
|
|
629
|
-
for (const file of
|
|
681
|
+
for (const file of ownManifests.filter((f) => /(^|\/)Gemfile$/.test(f))) {
|
|
630
682
|
const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
|
|
631
683
|
for (const line of raw.split('\n')) {
|
|
632
684
|
// Skip commented-out gems, which are otherwise indistinguishable from real ones.
|
|
@@ -725,6 +777,7 @@ async function analyzeProject(projectPath) {
|
|
|
725
777
|
phpDeps: unique(phpDeps),
|
|
726
778
|
goDeps: unique(goDeps),
|
|
727
779
|
rustDeps: unique(rustDeps),
|
|
780
|
+
runtimeRustDeps: unique(runtimeRustDeps),
|
|
728
781
|
rubyDeps: unique(rubyDeps),
|
|
729
782
|
dotnetDeps: unique(dotnetDeps),
|
|
730
783
|
dotnetWebSdk,
|
|
@@ -111,7 +111,7 @@ async function detectBackend(ctx) {
|
|
|
111
111
|
/** Rust, read from Cargo.toml. */
|
|
112
112
|
let namedRustFramework = false;
|
|
113
113
|
for (const [framework, deps] of catalogue_1.RUST_BACKEND_FRAMEWORKS) {
|
|
114
|
-
const hits = (0, detectContext_1.
|
|
114
|
+
const hits = (0, detectContext_1.hasAnyRuntimeRustDep)(ctx, deps);
|
|
115
115
|
if (!hits.length)
|
|
116
116
|
continue;
|
|
117
117
|
namedRustFramework = true;
|
|
@@ -28,6 +28,8 @@ export interface DetectContext {
|
|
|
28
28
|
goDeps: string[];
|
|
29
29
|
/** Crate names from Cargo.toml, lowercase. */
|
|
30
30
|
rustDeps: string[];
|
|
31
|
+
/** Of those, the ones the product ships with rather than only builds and tests with. */
|
|
32
|
+
runtimeRustDeps: string[];
|
|
31
33
|
/** Gem names from the Gemfile, lowercase. */
|
|
32
34
|
rubyDeps: string[];
|
|
33
35
|
/** PackageReference and FrameworkReference names from .csproj, lowercase. */
|
|
@@ -74,6 +76,8 @@ export declare function hasRuntimePyDep(ctx: DetectContext, name: string): boole
|
|
|
74
76
|
export declare function hasAnyDep(ctx: DetectContext, names: string[]): string[];
|
|
75
77
|
export declare function hasPyDep(ctx: DetectContext, name: string): boolean;
|
|
76
78
|
export declare function hasAnyRustDep(ctx: DetectContext, names: string[]): string[];
|
|
79
|
+
/** The Rust counterpart of `hasRuntimeDep`: shipped, not merely present. */
|
|
80
|
+
export declare function hasAnyRuntimeRustDep(ctx: DetectContext, names: string[]): string[];
|
|
77
81
|
export declare function hasAnyPyDep(ctx: DetectContext, names: string[]): string[];
|
|
78
82
|
export declare function hasPhpDep(ctx: DetectContext, name: string): boolean;
|
|
79
83
|
export declare function hasAnyPhpDep(ctx: DetectContext, names: string[]): string[];
|
|
@@ -6,6 +6,7 @@ exports.hasRuntimePyDep = hasRuntimePyDep;
|
|
|
6
6
|
exports.hasAnyDep = hasAnyDep;
|
|
7
7
|
exports.hasPyDep = hasPyDep;
|
|
8
8
|
exports.hasAnyRustDep = hasAnyRustDep;
|
|
9
|
+
exports.hasAnyRuntimeRustDep = hasAnyRuntimeRustDep;
|
|
9
10
|
exports.hasAnyPyDep = hasAnyPyDep;
|
|
10
11
|
exports.hasPhpDep = hasPhpDep;
|
|
11
12
|
exports.hasAnyPhpDep = hasAnyPhpDep;
|
|
@@ -40,6 +41,10 @@ function hasPyDep(ctx, name) {
|
|
|
40
41
|
function hasAnyRustDep(ctx, names) {
|
|
41
42
|
return names.filter((name) => ctx.rustDeps.includes(name.toLowerCase()));
|
|
42
43
|
}
|
|
44
|
+
/** The Rust counterpart of `hasRuntimeDep`: shipped, not merely present. */
|
|
45
|
+
function hasAnyRuntimeRustDep(ctx, names) {
|
|
46
|
+
return names.filter((name) => ctx.runtimeRustDeps.includes(name.toLowerCase()));
|
|
47
|
+
}
|
|
43
48
|
function hasAnyPyDep(ctx, names) {
|
|
44
49
|
return names.filter((n) => hasPyDep(ctx, n));
|
|
45
50
|
}
|
|
@@ -134,10 +134,23 @@ function buildVerdict(args) {
|
|
|
134
134
|
launchReady: null,
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
|
-
|
|
137
|
+
/**
|
|
138
|
+
* No profile applied is not a profile satisfied.
|
|
139
|
+
*
|
|
140
|
+
* An inconclusive inference produces a profile record with an empty gap — nothing
|
|
141
|
+
* required, so nothing missing — and the verdict read "This project covers
|
|
142
|
+
* everything expected of Auto (inconclusive). What remains is refinement, not
|
|
143
|
+
* blockers." meilisearch, a search engine somebody self-hosts, was told it was ready
|
|
144
|
+
* to launch because no expectations had been applied to it.
|
|
145
|
+
*
|
|
146
|
+
* It is the same mistake as every other one this run has found, in the strongest
|
|
147
|
+
* possible direction: absence read as satisfaction.
|
|
148
|
+
*/
|
|
149
|
+
if (!args.profile || args.profile.selectedProfile === 'auto') {
|
|
138
150
|
return {
|
|
139
151
|
verdict: `Judged on its code alone, this project looks like ${MATURITY_LABEL[args.maturity]}. Choose a product profile to see what it would still need to launch.`,
|
|
140
|
-
|
|
152
|
+
// Launch readiness is a question about a profile. Without one it was not asked.
|
|
153
|
+
launchReady: args.profile ? null : args.maturity === 'production_ready',
|
|
141
154
|
};
|
|
142
155
|
}
|
|
143
156
|
const { gap, profileTitle } = args.profile;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.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": {
|