@produtype/core 0.71.0 → 0.73.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/dist/analyzer/analyzeProject.js +2 -0
- package/dist/analyzer/detectBackend.js +17 -1
- package/dist/analyzer/readingDepth.d.ts +9 -2
- package/dist/analyzer/readingDepth.js +37 -6
- package/dist/analyzer/structural/loadTypeScript.d.ts +11 -0
- package/dist/analyzer/structural/loadTypeScript.js +14 -0
- package/dist/analyzer/types.d.ts +8 -0
- package/dist/expectations/profileSignals.js +17 -1
- package/dist/report/buildReport.js +2 -1
- package/dist/report/markdownReport.js +2 -2
- package/dist/report/types.d.ts +8 -0
- package/package.json +1 -1
|
@@ -65,6 +65,7 @@ const detectNotifications_1 = require("./detectNotifications");
|
|
|
65
65
|
const detectDeployment_1 = require("./detectDeployment");
|
|
66
66
|
const detectStack_1 = require("./detectStack");
|
|
67
67
|
const promises_1 = require("node:fs/promises");
|
|
68
|
+
const loadTypeScript_1 = require("./structural/loadTypeScript");
|
|
68
69
|
const packageJsonSchema = zod_1.z
|
|
69
70
|
.object({
|
|
70
71
|
name: zod_1.z.string().optional(),
|
|
@@ -841,6 +842,7 @@ async function analyzeProject(projectPath) {
|
|
|
841
842
|
return {
|
|
842
843
|
projectPath: root,
|
|
843
844
|
scannedAt: new Date().toISOString(),
|
|
845
|
+
parsedStructure: await (0, loadTypeScript_1.typeScriptIsAvailable)(),
|
|
844
846
|
stack: (0, detectStack_1.buildStackInfo)({
|
|
845
847
|
frontend: frontend.frameworks,
|
|
846
848
|
backend: backend.frameworks,
|
|
@@ -142,7 +142,23 @@ async function detectBackend(ctx) {
|
|
|
142
142
|
for (const dep of hits)
|
|
143
143
|
evidence.push({ type: 'dependency', value: dep });
|
|
144
144
|
}
|
|
145
|
-
|
|
145
|
+
/**
|
|
146
|
+
* A Gemfile is not a Ruby backend on its own.
|
|
147
|
+
*
|
|
148
|
+
* WordPress-iOS and BlueWallet both reported `backend: ruby`. Neither ships a Ruby
|
|
149
|
+
* server: both keep a Gemfile for fastlane and CocoaPods, which is how the iOS
|
|
150
|
+
* world runs its build. WordPress-iOS has 23 Ruby files among 2675 — under one per
|
|
151
|
+
* cent — and the backend it did not have then excluded it from the mobile profile,
|
|
152
|
+
* so an iOS application with 2649 Swift files was judged as a B2B SaaS and told it
|
|
153
|
+
* needed a health endpoint.
|
|
154
|
+
*
|
|
155
|
+
* The same share test Go already uses, and the reason Rust's fallback was removed
|
|
156
|
+
* outright a release ago: a language has to be a real part of what is written here
|
|
157
|
+
* before it names the backend.
|
|
158
|
+
*/
|
|
159
|
+
if (!namedRubyFramework
|
|
160
|
+
&& languageShare(ctx, /\.rb$/) >= MINIMUM_BACKEND_SHARE
|
|
161
|
+
&& ctx.files.all.some((f) => /(^|\/)Gemfile$/.test(f))) {
|
|
146
162
|
frameworks.push('ruby');
|
|
147
163
|
evidence.push({ type: 'note', value: 'a Gemfile with no web framework in it' });
|
|
148
164
|
}
|
|
@@ -34,12 +34,19 @@ export interface LanguageReading {
|
|
|
34
34
|
export declare function readingDepths(sourceFiles: string[], unreadable: Array<{
|
|
35
35
|
language: string;
|
|
36
36
|
files: number;
|
|
37
|
-
}
|
|
37
|
+
}>, parserAvailable: boolean): LanguageReading[];
|
|
38
38
|
/**
|
|
39
39
|
* The sentence a reader needs, or nothing.
|
|
40
40
|
*
|
|
41
41
|
* Silent where everything was parsed: a report that congratulates itself on reading
|
|
42
42
|
* properly is noise. It speaks when some of the reading was shallower than the rest,
|
|
43
43
|
* which is the case that misleads.
|
|
44
|
+
*
|
|
45
|
+
* `parserAvailable` separates two shallow readings that look identical in the list and
|
|
46
|
+
* are not. Go is searched because nothing here will ever parse Go, and a reader can do
|
|
47
|
+
* nothing about it. JavaScript is searched only when the optional compiler is missing,
|
|
48
|
+
* and installing it changes the answer — on the fixture corpus, seven repositories of a
|
|
49
|
+
* hundred and thirty-three answer differently. Telling a reader to install a package is
|
|
50
|
+
* worth a clause; telling them their Go is read by keyword is worth a different one.
|
|
44
51
|
*/
|
|
45
|
-
export declare function describeReadingDepth(readings: LanguageReading[]): string | undefined;
|
|
52
|
+
export declare function describeReadingDepth(readings: LanguageReading[], parserAvailable: boolean): string | undefined;
|
|
@@ -11,10 +11,25 @@ const catalogue_1 = require("./catalogue");
|
|
|
11
11
|
* within a week, which is the failure this file exists to stop somebody else making.
|
|
12
12
|
*/
|
|
13
13
|
const PARSED_EXTENSIONS = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The catalogue labels those extensions carry, so the missing-compiler clause fires for
|
|
16
|
+
* the languages it can actually do something about and stays quiet on a Go repository.
|
|
17
|
+
*/
|
|
18
|
+
const PARSED_EXTENSIONS_LANGUAGES = new Set(catalogue_1.LANGUAGES.filter(({ extensions }) => ['a.ts', 'a.tsx', 'a.js', 'a.jsx', 'a.mjs', 'a.cjs'].some((name) => extensions.test(name))).map(({ label }) => label));
|
|
19
|
+
/**
|
|
20
|
+
* Languages whose files are read as text but never parsed.
|
|
21
|
+
*
|
|
22
|
+
* The extension says a parser *could* read this file; `parserAvailable` says one
|
|
23
|
+
* actually did. The optional TypeScript peer is absent on any machine that installed
|
|
24
|
+
* this package without it — `npx prodkit` against a repository is the ordinary case —
|
|
25
|
+
* and until this argument existed the report claimed `parsed` there just the same, on
|
|
26
|
+
* the strength of the file name.
|
|
27
|
+
*/
|
|
28
|
+
function depthFor(files, parserAvailable) {
|
|
16
29
|
if (files.length === 0)
|
|
17
30
|
return 'skipped';
|
|
31
|
+
if (!parserAvailable)
|
|
32
|
+
return 'searched';
|
|
18
33
|
return files.every((file) => PARSED_EXTENSIONS.test(file)) ? 'parsed' : 'searched';
|
|
19
34
|
}
|
|
20
35
|
/**
|
|
@@ -24,13 +39,13 @@ function depthFor(files) {
|
|
|
24
39
|
* directory was not read because it was not the project's, which is a different fact and
|
|
25
40
|
* one the reader does not need here.
|
|
26
41
|
*/
|
|
27
|
-
function readingDepths(sourceFiles, unreadable) {
|
|
42
|
+
function readingDepths(sourceFiles, unreadable, parserAvailable) {
|
|
28
43
|
const readings = [];
|
|
29
44
|
for (const { label, extensions } of catalogue_1.LANGUAGES) {
|
|
30
45
|
const files = sourceFiles.filter((file) => extensions.test(file));
|
|
31
46
|
if (files.length === 0)
|
|
32
47
|
continue;
|
|
33
|
-
readings.push({ language: label, files: files.length, depth: depthFor(files) });
|
|
48
|
+
readings.push({ language: label, files: files.length, depth: depthFor(files, parserAvailable) });
|
|
34
49
|
}
|
|
35
50
|
for (const entry of unreadable) {
|
|
36
51
|
readings.push({ language: entry.language, files: entry.files, depth: 'skipped' });
|
|
@@ -43,8 +58,15 @@ function readingDepths(sourceFiles, unreadable) {
|
|
|
43
58
|
* Silent where everything was parsed: a report that congratulates itself on reading
|
|
44
59
|
* properly is noise. It speaks when some of the reading was shallower than the rest,
|
|
45
60
|
* which is the case that misleads.
|
|
61
|
+
*
|
|
62
|
+
* `parserAvailable` separates two shallow readings that look identical in the list and
|
|
63
|
+
* are not. Go is searched because nothing here will ever parse Go, and a reader can do
|
|
64
|
+
* nothing about it. JavaScript is searched only when the optional compiler is missing,
|
|
65
|
+
* and installing it changes the answer — on the fixture corpus, seven repositories of a
|
|
66
|
+
* hundred and thirty-three answer differently. Telling a reader to install a package is
|
|
67
|
+
* worth a clause; telling them their Go is read by keyword is worth a different one.
|
|
46
68
|
*/
|
|
47
|
-
function describeReadingDepth(readings) {
|
|
69
|
+
function describeReadingDepth(readings, parserAvailable) {
|
|
48
70
|
const searched = readings.filter((entry) => entry.depth === 'searched');
|
|
49
71
|
const skipped = readings.filter((entry) => entry.depth === 'skipped');
|
|
50
72
|
if (searched.length === 0 && skipped.length === 0)
|
|
@@ -56,5 +78,14 @@ function describeReadingDepth(readings) {
|
|
|
56
78
|
if (skipped.length > 0) {
|
|
57
79
|
parts.push(`${skipped.map((entry) => `${entry.language} (${entry.files} files)`).join(', ')} not read at all`);
|
|
58
80
|
}
|
|
59
|
-
|
|
81
|
+
const missingParser = !parserAvailable && readings.some((entry) => PARSED_EXTENSIONS_LANGUAGES.has(entry.language));
|
|
82
|
+
return [
|
|
83
|
+
`How this repository was read: ${parts.join('; ')}.`,
|
|
84
|
+
'A keyword can appear in a comment, a test fixture or a variable name, so findings in those languages rest on weaker evidence than the ones this analyzer parsed.',
|
|
85
|
+
...(missingParser
|
|
86
|
+
? [
|
|
87
|
+
'The optional `typescript` peer dependency is not installed, so no JavaScript or TypeScript was parsed here either: every structural question — which literal reaches a signing call, whether a comparison guards a route or picks a label — went unasked. Install it alongside this package and re-run to get those answers.',
|
|
88
|
+
]
|
|
89
|
+
: []),
|
|
90
|
+
].join(' ');
|
|
60
91
|
}
|
|
@@ -2,3 +2,14 @@ import type * as TypeScriptApi from 'typescript';
|
|
|
2
2
|
export declare function loadTypeScript(): Promise<typeof TypeScriptApi | null>;
|
|
3
3
|
/** For tests that need to observe both paths without reinstalling anything. */
|
|
4
4
|
export declare function resetTypeScriptCache(): void;
|
|
5
|
+
/**
|
|
6
|
+
* Whether the parser is there, for the report rather than for a reader.
|
|
7
|
+
*
|
|
8
|
+
* `readingDepths` printed `parsed` for every `.ts` and `.js` file on the strength of the
|
|
9
|
+
* extension alone, and said nothing when everything was parsed — so on a machine without
|
|
10
|
+
* the optional compiler the report was silent in exactly the case it needed to speak.
|
|
11
|
+
* Measured on the fixture corpus: seven of a hundred and thirty-three repositories
|
|
12
|
+
* answer differently with the compiler hidden, and one of them reports a hardcoded
|
|
13
|
+
* signing secret as `passed`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function typeScriptIsAvailable(): Promise<boolean>;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.loadTypeScript = loadTypeScript;
|
|
4
4
|
exports.resetTypeScriptCache = resetTypeScriptCache;
|
|
5
|
+
exports.typeScriptIsAvailable = typeScriptIsAvailable;
|
|
5
6
|
/**
|
|
6
7
|
* The TypeScript compiler, loaded when something needs to read structure rather than text.
|
|
7
8
|
*
|
|
@@ -39,3 +40,16 @@ async function loadTypeScript() {
|
|
|
39
40
|
function resetTypeScriptCache() {
|
|
40
41
|
cached = undefined;
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Whether the parser is there, for the report rather than for a reader.
|
|
45
|
+
*
|
|
46
|
+
* `readingDepths` printed `parsed` for every `.ts` and `.js` file on the strength of the
|
|
47
|
+
* extension alone, and said nothing when everything was parsed — so on a machine without
|
|
48
|
+
* the optional compiler the report was silent in exactly the case it needed to speak.
|
|
49
|
+
* Measured on the fixture corpus: seven of a hundred and thirty-three repositories
|
|
50
|
+
* answer differently with the compiler hidden, and one of them reports a hardcoded
|
|
51
|
+
* signing secret as `passed`.
|
|
52
|
+
*/
|
|
53
|
+
async function typeScriptIsAvailable() {
|
|
54
|
+
return (await loadTypeScript()) !== null;
|
|
55
|
+
}
|
package/dist/analyzer/types.d.ts
CHANGED
|
@@ -117,4 +117,12 @@ export interface ProjectAnalysis {
|
|
|
117
117
|
workspaceStacks: WorkspaceStack[];
|
|
118
118
|
files: ProjectFiles;
|
|
119
119
|
detectors: Record<string, DetectorResult>;
|
|
120
|
+
/**
|
|
121
|
+
* Whether the optional TypeScript compiler was there while the detectors ran.
|
|
122
|
+
*
|
|
123
|
+
* Carried on the analysis rather than asked for at report time, because it is a fact
|
|
124
|
+
* about this reading and not about the machine printing it: the report's claim of how
|
|
125
|
+
* deeply it read has to match what the detectors were actually able to do.
|
|
126
|
+
*/
|
|
127
|
+
parsedStructure: boolean;
|
|
120
128
|
}
|
|
@@ -164,7 +164,23 @@ const RULES = [
|
|
|
164
164
|
* Half is the line, and it sits in the gap between those two rather than in the
|
|
165
165
|
* middle of a distribution.
|
|
166
166
|
*/
|
|
167
|
-
|
|
167
|
+
/**
|
|
168
|
+
* A tenant word in a client model is not a tenant boundary.
|
|
169
|
+
*
|
|
170
|
+
* WordPress-iOS carries `organizationID` in `RemoteBlog.swift` and
|
|
171
|
+
* `RemoteReaderSiteInfo.swift` — data classes deserialised from WordPress.com's
|
|
172
|
+
* JSON. The application consumes an organization; it does not host one, and it
|
|
173
|
+
* could not: enforcing a boundary between tenants takes a server, and this is
|
|
174
|
+
* 2649 Swift files with none.
|
|
175
|
+
*
|
|
176
|
+
* It was excluded from the mobile profile on that word and judged as a B2B SaaS,
|
|
177
|
+
* which asked it for a health endpoint, security headers and a GDPR export
|
|
178
|
+
* route. Tenancy still disqualifies a phone application that ships a server
|
|
179
|
+
* alongside it, because then the boundary is the repository's to keep.
|
|
180
|
+
*/
|
|
181
|
+
admissible: (f) => f.mobilePlatforms.length > 0
|
|
182
|
+
&& f.mobileShare >= 0.5
|
|
183
|
+
&& !(f.tenancy && f.productBackend),
|
|
168
184
|
signals: [
|
|
169
185
|
{ identifies: true, label: 'a mobile project in the repository', weight: 5, holds: (f) => f.mobilePlatforms.length > 0 },
|
|
170
186
|
{
|
|
@@ -373,7 +373,8 @@ function buildReport(analysis, options) {
|
|
|
373
373
|
* its severity. A keyword match in Go and a parsed guard in TypeScript were being
|
|
374
374
|
* presented with the same confidence.
|
|
375
375
|
*/
|
|
376
|
-
readingDepth: (0, readingDepth_1.readingDepths)(analysis.files.source, analysis.files.unreadable),
|
|
376
|
+
readingDepth: (0, readingDepth_1.readingDepths)(analysis.files.source, analysis.files.unreadable, analysis.parsedStructure),
|
|
377
|
+
parsedStructure: analysis.parsedStructure,
|
|
377
378
|
detectors: detectorDiagnostics(analysis),
|
|
378
379
|
selectedProfile: requestedProfile,
|
|
379
380
|
inferredProfile: productProfile?.inferredProfile,
|
|
@@ -274,8 +274,8 @@ function renderMarkdown(report) {
|
|
|
274
274
|
* confidence, and nothing in the report distinguished them. Silent when everything
|
|
275
275
|
* was parsed: a report congratulating itself on reading properly is noise.
|
|
276
276
|
*/
|
|
277
|
-
...((0, readingDepth_1.describeReadingDepth)(report.diagnostics.readingDepth)
|
|
278
|
-
? [`- ${(0, readingDepth_1.describeReadingDepth)(report.diagnostics.readingDepth)}`]
|
|
277
|
+
...((0, readingDepth_1.describeReadingDepth)(report.diagnostics.readingDepth, report.diagnostics.parsedStructure)
|
|
278
|
+
? [`- ${(0, readingDepth_1.describeReadingDepth)(report.diagnostics.readingDepth, report.diagnostics.parsedStructure)}`]
|
|
279
279
|
: []),
|
|
280
280
|
`- Expectation mode: ${report.diagnostics.expectationMode}`,
|
|
281
281
|
`- ProdKit version: ${report.diagnostics.prodkitVersion}`,
|
package/dist/report/types.d.ts
CHANGED
|
@@ -74,6 +74,14 @@ export interface ReportDiagnostics {
|
|
|
74
74
|
* fact, and the reader is entitled to know which one they have.
|
|
75
75
|
*/
|
|
76
76
|
readingDepth: LanguageReading[];
|
|
77
|
+
/**
|
|
78
|
+
* Whether the optional TypeScript compiler was loaded for this reading.
|
|
79
|
+
*
|
|
80
|
+
* Two readings with the same `readingDepth` list are not the same reading: JavaScript
|
|
81
|
+
* shows as `searched` only when the compiler was missing, and that is the one a reader
|
|
82
|
+
* can fix.
|
|
83
|
+
*/
|
|
84
|
+
parsedStructure: boolean;
|
|
77
85
|
detectors: Array<{
|
|
78
86
|
id: string;
|
|
79
87
|
status: 'completed' | 'skipped';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.73.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": {
|