@produtype/core 0.31.0 → 0.33.1
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/README.md +2 -1
- package/dist/analyzer/analyzeProject.js +38 -5
- package/dist/analyzer/catalogue.js +10 -1
- package/dist/analyzer/detectBackend.js +30 -4
- package/dist/analyzer/detectContext.d.ts +12 -0
- package/dist/analyzer/detectContext.js +5 -0
- package/dist/analyzer/detectPackaging.d.ts +10 -0
- package/dist/analyzer/detectPackaging.js +59 -4
- package/dist/expectations/profileSignals.d.ts +8 -0
- package/dist/expectations/profileSignals.js +21 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -174,7 +174,7 @@ prodkit plan ../my-app --output prodkit-plan.md
|
|
|
174
174
|
|
|
175
175
|
_Generated from the analyzer itself — run `npm run docs:stacks` after changing a detector._
|
|
176
176
|
|
|
177
|
-
- **Backend:** Express, Next.js, NestJS, Fastify, Hono, Elysia, Koa, AdonisJS, SvelteKit, Remix, Nuxt, Nitro, Astro, Django, Flask, FastAPI, Litestar, Sanic, Tornado,
|
|
177
|
+
- **Backend:** Express, Next.js, NestJS, Fastify, Hono, Elysia, Koa, AdonisJS, SvelteKit, Remix, Nuxt, Nitro, Astro, Django, Flask, FastAPI, aiohttp, Litestar, Sanic, Tornado, Starlette, Streamlit, Gradio, Dash, Chainlit, Gin, Echo, Fiber, chi, Gorilla, Beego, Go, Rails, Sinatra, Hanami, Roda, Grape, Ruby, Laravel, Symfony, Slim, CodeIgniter, CakePHP, Yii, PHP, ASP.NET Core, .NET
|
|
178
178
|
- **Frontend:** React, Vite, Vue, Nuxt, Svelte, Angular, Astro, Solid, Qwik, Preact, Remix, htmx, Tailwind CSS, Electron
|
|
179
179
|
- **Mobile:** Flutter, React Native, iOS (native), Android (native)
|
|
180
180
|
- **Databases:** Postgres, MySQL, SQLite, SQL Server, MongoDB, Redis, Firestore, DynamoDB, Convex
|
|
@@ -188,6 +188,7 @@ How some of these are decided:
|
|
|
188
188
|
- **Django** — manage.py, settings.py and urls.py together
|
|
189
189
|
- **Flask** — the dependency, or an import in the source
|
|
190
190
|
- **FastAPI** — the dependency, or an import in the source
|
|
191
|
+
- **aiohttp** — aiohttp.web in the source — the dependency alone is a client
|
|
191
192
|
- **Go** — a go.mod with no framework in it — net/http is a real answer
|
|
192
193
|
- **Ruby** — a Gemfile with no web framework in it
|
|
193
194
|
- **PHP** — PHP sources with no framework in composer.json
|
|
@@ -87,7 +87,24 @@ function parseRequirements(text) {
|
|
|
87
87
|
function normalizePyDepSpec(spec) {
|
|
88
88
|
return spec.split(/[\s<>=!~;[(]/)[0].trim().toLowerCase();
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* What a Python package installs by default, and what it merely offers.
|
|
92
|
+
*
|
|
93
|
+
* `[project.optional-dependencies]` and `[dependency-groups]` are extras: a library that
|
|
94
|
+
* integrates with FastAPI declares it there, and nobody installing the library gets a
|
|
95
|
+
* web server. langchain and llama_index declare four web frameworks between them that
|
|
96
|
+
* way, and were read as web applications — costing langchain the `library` profile and
|
|
97
|
+
* earning it fifteen high findings about GDPR, billing and tenant isolation.
|
|
98
|
+
*
|
|
99
|
+
* Mirrors what `dependencies` and `devDependencies` already do for npm.
|
|
100
|
+
*/
|
|
101
|
+
function parsePyprojectRuntime(text) {
|
|
102
|
+
return parsePyprojectSections(text, true);
|
|
103
|
+
}
|
|
90
104
|
function parsePyproject(text) {
|
|
105
|
+
return parsePyprojectSections(text, false);
|
|
106
|
+
}
|
|
107
|
+
function parsePyprojectSections(text, runtimeOnly) {
|
|
91
108
|
if (!text)
|
|
92
109
|
return [];
|
|
93
110
|
const deps = [];
|
|
@@ -105,8 +122,8 @@ function parsePyproject(text) {
|
|
|
105
122
|
}
|
|
106
123
|
// Poetry-style tables: each key of the section is a dependency name.
|
|
107
124
|
const isPoetryDepsSection = section === 'tool.poetry.dependencies'
|
|
108
|
-
|| section === 'tool.poetry.dev-dependencies'
|
|
109
|
-
|
|
125
|
+
|| (!runtimeOnly && (section === 'tool.poetry.dev-dependencies'
|
|
126
|
+
|| /^tool\.poetry\.group\.[^.]+\.dependencies$/.test(section)));
|
|
110
127
|
if (isPoetryDepsSection) {
|
|
111
128
|
const match = line.match(/^([A-Za-z0-9_.-]+)\s*=/);
|
|
112
129
|
if (match && match[1].toLowerCase() !== 'python')
|
|
@@ -114,9 +131,10 @@ function parsePyproject(text) {
|
|
|
114
131
|
continue;
|
|
115
132
|
}
|
|
116
133
|
// PEP 621 / uv style: dependency specs live inside string arrays.
|
|
117
|
-
const isDepArraySection =
|
|
118
|
-
|
|
119
|
-
|
|
134
|
+
const isDepArraySection = !runtimeOnly
|
|
135
|
+
&& (section === 'project.optional-dependencies'
|
|
136
|
+
|| section === 'dependency-groups'
|
|
137
|
+
|| section === 'tool.uv');
|
|
120
138
|
if (section === 'project' || isDepArraySection || inDependencyArray) {
|
|
121
139
|
if (line.includes('include-group'))
|
|
122
140
|
continue;
|
|
@@ -366,10 +384,16 @@ async function analyzeProject(projectPath) {
|
|
|
366
384
|
requirementsDeps: parseRequirements(reqText),
|
|
367
385
|
pyprojectPath: pyprojectFiles.includes(pyprojectPath) ? pyprojectPath : undefined,
|
|
368
386
|
pyprojectDeps: parsePyproject(pyprojectText),
|
|
387
|
+
pyprojectRuntimeDeps: parsePyprojectRuntime(pyprojectText),
|
|
369
388
|
lockfiles: wsLockfiles,
|
|
370
389
|
});
|
|
371
390
|
}
|
|
372
391
|
const pythonDeps = unique(workspaces.flatMap((w) => [...w.requirementsDeps, ...w.pyprojectDeps]));
|
|
392
|
+
/**
|
|
393
|
+
* `requirements.txt` has no notion of an extra, so everything in it counts as shipped.
|
|
394
|
+
* Only pyproject distinguishes the two.
|
|
395
|
+
*/
|
|
396
|
+
const runtimePythonDeps = unique(workspaces.flatMap((w) => [...w.requirementsDeps, ...w.pyprojectRuntimeDeps]));
|
|
373
397
|
/**
|
|
374
398
|
* Composer requirements. A published PHP application in the corpus reported a
|
|
375
399
|
* backend of "unknown" because nothing read this file, while 57 source files sat
|
|
@@ -577,6 +601,14 @@ async function analyzeProject(projectPath) {
|
|
|
577
601
|
const imported = await (0, importedDependencies_1.pythonImports)(root, sourceFiles);
|
|
578
602
|
if (imported.length > 0) {
|
|
579
603
|
pythonDeps.push(...imported);
|
|
604
|
+
/**
|
|
605
|
+
* An import is use, not an offer.
|
|
606
|
+
*
|
|
607
|
+
* These count as shipped as well as present. A repository with no manifest at all
|
|
608
|
+
* — one `main.py` that imports streamlit — declares nothing optional, and treating
|
|
609
|
+
* its imports as extras made a Streamlit application unreadable.
|
|
610
|
+
*/
|
|
611
|
+
runtimePythonDeps.push(...imported);
|
|
580
612
|
inferredDependencySources.push('Python imports');
|
|
581
613
|
}
|
|
582
614
|
}
|
|
@@ -610,6 +642,7 @@ async function analyzeProject(projectPath) {
|
|
|
610
642
|
root,
|
|
611
643
|
files: { all: allFiles, source: sourceFiles, config: configFiles, unreadable: unreadableLanguages(allFiles) },
|
|
612
644
|
runtimeNpmDeps,
|
|
645
|
+
runtimePythonDeps,
|
|
613
646
|
packageJson,
|
|
614
647
|
pythonDeps,
|
|
615
648
|
phpDeps: unique(phpDeps),
|
|
@@ -69,7 +69,6 @@ exports.PYTHON_BACKEND_FRAMEWORKS = [
|
|
|
69
69
|
['litestar', 'litestar'],
|
|
70
70
|
['sanic', 'sanic'],
|
|
71
71
|
['tornado', 'tornado'],
|
|
72
|
-
['aiohttp', 'aiohttp'],
|
|
73
72
|
['starlette', 'starlette'],
|
|
74
73
|
/**
|
|
75
74
|
* These serve an application over HTTP without calling themselves web frameworks,
|
|
@@ -248,6 +247,16 @@ function supportedStacks() {
|
|
|
248
247
|
{ id: 'django', label: 'Django', detectedFrom: 'manage.py, settings.py and urls.py together' },
|
|
249
248
|
{ id: 'flask', label: 'Flask', detectedFrom: 'the dependency, or an import in the source' },
|
|
250
249
|
{ id: 'fastapi', label: 'FastAPI', detectedFrom: 'the dependency, or an import in the source' },
|
|
250
|
+
{
|
|
251
|
+
id: 'aiohttp',
|
|
252
|
+
label: 'aiohttp',
|
|
253
|
+
/**
|
|
254
|
+
* Listed separately because the dependency alone does not settle it: aiohttp is
|
|
255
|
+
* a client as often as a server, and thousands of packages depend on it to make
|
|
256
|
+
* requests. `aiohttp.web` is what says it is being served.
|
|
257
|
+
*/
|
|
258
|
+
detectedFrom: 'aiohttp.web in the source — the dependency alone is a client',
|
|
259
|
+
},
|
|
251
260
|
...entries(exports.PYTHON_BACKEND_FRAMEWORKS.map(([id]) => id)),
|
|
252
261
|
...entries(exports.GO_BACKEND_FRAMEWORKS.map(([id]) => id)),
|
|
253
262
|
{ id: 'go', label: 'Go', detectedFrom: 'a go.mod with no framework in it — net/http is a real answer' },
|
|
@@ -150,14 +150,40 @@ async function detectBackend(ctx) {
|
|
|
150
150
|
}
|
|
151
151
|
// Python backend frameworks detected purely from dependencies. Django, Flask and
|
|
152
152
|
// FastAPI have their own blocks below because each also has a source-level fallback.
|
|
153
|
+
/**
|
|
154
|
+
* Shipped, not offered as an extra — the Python half of the same rule.
|
|
155
|
+
*
|
|
156
|
+
* llama_index declares tornado, starlette, flask and fastapi among its optional
|
|
157
|
+
* integrations, and was read as running all four.
|
|
158
|
+
*/
|
|
153
159
|
for (const [framework, dep] of catalogue_1.PYTHON_BACKEND_FRAMEWORKS) {
|
|
154
|
-
if ((0, detectContext_1.
|
|
160
|
+
if ((0, detectContext_1.hasRuntimePyDep)(ctx, dep)) {
|
|
155
161
|
frameworks.push(framework);
|
|
156
162
|
evidence.push({ type: 'dependency', value: dep });
|
|
157
163
|
}
|
|
158
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* aiohttp is a client as often as it is a server.
|
|
167
|
+
*
|
|
168
|
+
* Thousands of packages depend on it to make HTTP requests; langchain is one, and was
|
|
169
|
+
* read as having a backend because of it — which cost it the `library` profile and
|
|
170
|
+
* earned it fifteen high findings about GDPR, billing and tenant isolation. The
|
|
171
|
+
* dependency says the library is present. Only `aiohttp.web` says it is being served.
|
|
172
|
+
*
|
|
173
|
+
* The source alone is enough, as elsewhere: a project that serves without declaring
|
|
174
|
+
* the dependency is still serving.
|
|
175
|
+
*/
|
|
176
|
+
if ((0, detectContext_1.hasRuntimePyDep)(ctx, 'aiohttp')) {
|
|
177
|
+
const serving = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source.filter((file) => /\.py$/.test(file)), [/aiohttp\.web/, /web\.Application\(/, /web\.RouteTableDef/, /from aiohttp import web/], 3);
|
|
178
|
+
if (serving.length) {
|
|
179
|
+
frameworks.push('aiohttp');
|
|
180
|
+
for (const match of serving) {
|
|
181
|
+
evidence.push({ type: 'snippet', value: match.snippet, file: match.file, line: match.line });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
159
185
|
// Flask
|
|
160
|
-
if ((0, detectContext_1.
|
|
186
|
+
if ((0, detectContext_1.hasRuntimePyDep)(ctx, 'flask')) {
|
|
161
187
|
frameworks.push('flask');
|
|
162
188
|
evidence.push({ type: 'dependency', value: 'flask' });
|
|
163
189
|
}
|
|
@@ -171,7 +197,7 @@ async function detectBackend(ctx) {
|
|
|
171
197
|
}
|
|
172
198
|
}
|
|
173
199
|
// FastAPI
|
|
174
|
-
if ((0, detectContext_1.
|
|
200
|
+
if ((0, detectContext_1.hasRuntimePyDep)(ctx, 'fastapi')) {
|
|
175
201
|
frameworks.push('fastapi');
|
|
176
202
|
evidence.push({ type: 'dependency', value: 'fastapi' });
|
|
177
203
|
}
|
|
@@ -192,7 +218,7 @@ async function detectBackend(ctx) {
|
|
|
192
218
|
const settingsFile = ctx.files.all.find((f) => f.endsWith('settings.py'));
|
|
193
219
|
if (settingsFile)
|
|
194
220
|
djangoSignals.push({ type: 'file', value: settingsFile });
|
|
195
|
-
if ((0, detectContext_1.
|
|
221
|
+
if ((0, detectContext_1.hasRuntimePyDep)(ctx, 'django'))
|
|
196
222
|
djangoSignals.push({ type: 'dependency', value: 'django' });
|
|
197
223
|
if (ctx.files.all.some((f) => f.endsWith('urls.py'))) {
|
|
198
224
|
djangoSignals.push({ type: 'file', value: ctx.files.all.find((f) => f.endsWith('urls.py')) });
|
|
@@ -7,6 +7,8 @@ export interface WorkspaceManifest {
|
|
|
7
7
|
requirementsDeps: string[];
|
|
8
8
|
pyprojectPath?: string;
|
|
9
9
|
pyprojectDeps: string[];
|
|
10
|
+
/** Of those, the ones installed by default rather than offered as an extra. */
|
|
11
|
+
pyprojectRuntimeDeps: string[];
|
|
10
12
|
lockfiles: string[];
|
|
11
13
|
}
|
|
12
14
|
export interface DetectContext {
|
|
@@ -47,6 +49,14 @@ export interface DetectContext {
|
|
|
47
49
|
* a backend, which kept it out of the `library` profile it plainly belongs to.
|
|
48
50
|
*/
|
|
49
51
|
runtimeNpmDeps: Record<string, string>;
|
|
52
|
+
/**
|
|
53
|
+
* Python packages installed by default, without the extras.
|
|
54
|
+
*
|
|
55
|
+
* A library that integrates with FastAPI declares it under
|
|
56
|
+
* `[project.optional-dependencies]`, and nobody installing the library gets a web
|
|
57
|
+
* server. Used for the same single question as `runtimeNpmDeps`.
|
|
58
|
+
*/
|
|
59
|
+
runtimePythonDeps: string[];
|
|
50
60
|
workspaces: WorkspaceManifest[];
|
|
51
61
|
}
|
|
52
62
|
export declare function hasDep(ctx: DetectContext, name: string): boolean;
|
|
@@ -57,6 +67,8 @@ export declare function hasDep(ctx: DetectContext, name: string): boolean;
|
|
|
57
67
|
* question "does this repository serve requests" is answered by what it ships.
|
|
58
68
|
*/
|
|
59
69
|
export declare function hasRuntimeDep(ctx: DetectContext, name: string): boolean;
|
|
70
|
+
/** The Python counterpart: shipped, not merely offered as an extra. */
|
|
71
|
+
export declare function hasRuntimePyDep(ctx: DetectContext, name: string): boolean;
|
|
60
72
|
export declare function hasAnyDep(ctx: DetectContext, names: string[]): string[];
|
|
61
73
|
export declare function hasPyDep(ctx: DetectContext, name: string): boolean;
|
|
62
74
|
export declare function hasAnyPyDep(ctx: DetectContext, names: string[]): string[];
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.hasDep = hasDep;
|
|
4
4
|
exports.hasRuntimeDep = hasRuntimeDep;
|
|
5
|
+
exports.hasRuntimePyDep = hasRuntimePyDep;
|
|
5
6
|
exports.hasAnyDep = hasAnyDep;
|
|
6
7
|
exports.hasPyDep = hasPyDep;
|
|
7
8
|
exports.hasAnyPyDep = hasAnyPyDep;
|
|
@@ -25,6 +26,10 @@ function hasDep(ctx, name) {
|
|
|
25
26
|
function hasRuntimeDep(ctx, name) {
|
|
26
27
|
return Object.prototype.hasOwnProperty.call(ctx.runtimeNpmDeps, name.toLowerCase());
|
|
27
28
|
}
|
|
29
|
+
/** The Python counterpart: shipped, not merely offered as an extra. */
|
|
30
|
+
function hasRuntimePyDep(ctx, name) {
|
|
31
|
+
return ctx.runtimePythonDeps.includes(name.toLowerCase());
|
|
32
|
+
}
|
|
28
33
|
function hasAnyDep(ctx, names) {
|
|
29
34
|
return names.filter((n) => hasDep(ctx, n));
|
|
30
35
|
}
|
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
import type { DetectorResult } from './types';
|
|
2
2
|
import type { DetectContext } from './detectContext';
|
|
3
|
+
/**
|
|
4
|
+
* Where a documentation site lives when it has no generator of its own.
|
|
5
|
+
*
|
|
6
|
+
* A hand-built docs site or a playground sits in a directory named for what it is. This
|
|
7
|
+
* is only consulted for the front-end files: a `docs/` folder full of Markdown says
|
|
8
|
+
* nothing either way, and a repository whose application happens to live under `www/`
|
|
9
|
+
* is not caught because the question asked is whether *every* front-end file is in one
|
|
10
|
+
* of these.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DOCS_DIRECTORIES: RegExp;
|
|
3
13
|
export declare function detectPackaging(ctx: DetectContext): Promise<DetectorResult[]>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DOCS_DIRECTORIES = void 0;
|
|
3
4
|
exports.detectPackaging = detectPackaging;
|
|
4
5
|
const readTextFileSafe_1 = require("../utils/readTextFileSafe");
|
|
5
6
|
const detectContext_1 = require("./detectContext");
|
|
7
|
+
const readTextFileSafe_2 = require("../utils/readTextFileSafe");
|
|
6
8
|
/**
|
|
7
9
|
* Whether a project is fit to be installed and depended on by someone else.
|
|
8
10
|
*
|
|
@@ -55,8 +57,29 @@ const DOCS_GENERATORS = [
|
|
|
55
57
|
* is not caught because the question asked is whether *every* front-end file is in one
|
|
56
58
|
* of these.
|
|
57
59
|
*/
|
|
58
|
-
|
|
60
|
+
exports.DOCS_DIRECTORIES = /(^|\/)(docs?|website|playground|examples?|demo|www)\//i;
|
|
59
61
|
const FRONTEND_FILE = /\.(tsx|jsx|vue|svelte|astro)$/;
|
|
62
|
+
/**
|
|
63
|
+
* The published package inside a workspace, if there is one.
|
|
64
|
+
*
|
|
65
|
+
* Named, versioned, not private — the three things npm requires to accept a publish.
|
|
66
|
+
* A workspace full of private example applications has none, and gets the honest answer
|
|
67
|
+
* that nothing here is published.
|
|
68
|
+
*/
|
|
69
|
+
async function findPublishedMember(ctx) {
|
|
70
|
+
const members = ctx.files.all
|
|
71
|
+
.filter((file) => /(^|\/)package\.json$/.test(file) && file !== 'package.json')
|
|
72
|
+
.slice(0, 60);
|
|
73
|
+
for (const file of members) {
|
|
74
|
+
const manifest = await (0, readTextFileSafe_2.readJsonSafe)(ctx.root, file);
|
|
75
|
+
if (!manifest)
|
|
76
|
+
continue;
|
|
77
|
+
const named = typeof manifest.name === 'string' && typeof manifest.version === 'string';
|
|
78
|
+
if (named && manifest.private !== true)
|
|
79
|
+
return { path: file, manifest };
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
60
83
|
async function detectPackaging(ctx) {
|
|
61
84
|
const all = ctx.files.all;
|
|
62
85
|
/**
|
|
@@ -71,7 +94,7 @@ async function detectPackaging(ctx) {
|
|
|
71
94
|
const docsGenerators = [...(0, detectContext_1.hasAnyDep)(ctx, DOCS_GENERATORS), ...(0, detectContext_1.hasAnyPyDep)(ctx, DOCS_GENERATORS)];
|
|
72
95
|
const frontendFiles = ctx.files.source.filter((file) => FRONTEND_FILE.test(file));
|
|
73
96
|
const frontendAllInDocs = frontendFiles.length > 0
|
|
74
|
-
&& frontendFiles.every((file) => DOCS_DIRECTORIES.test(file));
|
|
97
|
+
&& frontendFiles.every((file) => exports.DOCS_DIRECTORIES.test(file));
|
|
75
98
|
const documentationSite = docsGenerators.length > 0 || frontendAllInDocs;
|
|
76
99
|
const licenseFiles = collect(all, LICENSE_FILE);
|
|
77
100
|
const declaredLicense = typeof ctx.packageJson?.license === 'string';
|
|
@@ -111,7 +134,33 @@ async function detectPackaging(ctx) {
|
|
|
111
134
|
* it starts; `types` is how a TypeScript consumer finds it. A Python package says the
|
|
112
135
|
* same thing through `[project.scripts]` or a `packages` argument in setup.py.
|
|
113
136
|
*/
|
|
114
|
-
const
|
|
137
|
+
const rootPkg = (ctx.packageJson ?? {});
|
|
138
|
+
/**
|
|
139
|
+
* In a monorepo the root manifest is not the package.
|
|
140
|
+
*
|
|
141
|
+
* zod's root is `private: true` with `workspaces: ["packages/*"]`, no name, no version
|
|
142
|
+
* and no entry point; `zod` itself is `packages/zod/package.json`, named, versioned
|
|
143
|
+
* and exported. Reading only the root made the most downloaded validation library on
|
|
144
|
+
* npm a repository that publishes nothing — and vite the same.
|
|
145
|
+
*
|
|
146
|
+
* The published package is looked for among the members, and the first one that is
|
|
147
|
+
* named, versioned and not private is what this repository ships. Its manifest is
|
|
148
|
+
* what the evidence points at, so a reader can open the file the claim rests on rather
|
|
149
|
+
* than a root that says nothing.
|
|
150
|
+
*/
|
|
151
|
+
/**
|
|
152
|
+
* Three ways to say the same thing.
|
|
153
|
+
*
|
|
154
|
+
* npm and yarn declare workspaces inside `package.json`; pnpm puts them in
|
|
155
|
+
* `pnpm-workspace.yaml`. Reading only the first meant vite — whose root has no
|
|
156
|
+
* `workspaces` field at all — was never looked into, and came back as a repository
|
|
157
|
+
* that publishes nothing.
|
|
158
|
+
*/
|
|
159
|
+
const workspaceRoot = Array.isArray(rootPkg.workspaces)
|
|
160
|
+
|| (typeof rootPkg.workspaces === 'object' && rootPkg.workspaces !== null)
|
|
161
|
+
|| all.some((file) => /^(pnpm-workspace\.yaml|lerna\.json|nx\.json|turbo\.json)$/.test(file));
|
|
162
|
+
const publishedMember = workspaceRoot ? await findPublishedMember(ctx) : null;
|
|
163
|
+
const pkg = publishedMember?.manifest ?? rootPkg;
|
|
115
164
|
const nodeEntrypoints = ['main', 'module', 'exports', 'bin'].filter((key) => pkg[key] !== undefined);
|
|
116
165
|
const hasTypes = pkg.types !== undefined || pkg.typings !== undefined;
|
|
117
166
|
const isPrivate = pkg.private === true;
|
|
@@ -137,7 +186,13 @@ async function detectPackaging(ctx) {
|
|
|
137
186
|
key: 'packaging.manifest',
|
|
138
187
|
present: hasManifest,
|
|
139
188
|
complete: hasManifest && (named || pythonEntrypoints),
|
|
140
|
-
evidence: hasManifest
|
|
189
|
+
evidence: hasManifest
|
|
190
|
+
? [
|
|
191
|
+
publishedMember
|
|
192
|
+
? { type: 'file', value: `${publishedMember.path} names and versions the published package`, file: publishedMember.path }
|
|
193
|
+
: { type: 'note', value: named ? 'a manifest with a name and a version' : 'a package manifest' },
|
|
194
|
+
]
|
|
195
|
+
: [],
|
|
141
196
|
details: { named, described, sourced, private: isPrivate },
|
|
142
197
|
},
|
|
143
198
|
{
|
|
@@ -57,6 +57,14 @@ export interface ProfileFacts {
|
|
|
57
57
|
* the product.
|
|
58
58
|
*/
|
|
59
59
|
documentationSite: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* A server the product ships, as distinct from one its documentation or playgrounds run.
|
|
62
|
+
*
|
|
63
|
+
* zod's only workspace with a backend is `packages/docs`. vite's are ten directories
|
|
64
|
+
* under `playground/`. Both were excluded from the `library` profile by a backend
|
|
65
|
+
* neither of them ships.
|
|
66
|
+
*/
|
|
67
|
+
productBackend: boolean;
|
|
60
68
|
/** Somewhere for a consumer to import: main, module, exports, bin, a console script. */
|
|
61
69
|
entrypoints: boolean;
|
|
62
70
|
packagedLicense: boolean;
|
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.readFacts = readFacts;
|
|
4
4
|
exports.scoreProfiles = scoreProfiles;
|
|
5
|
+
const detectPackaging_1 = require("../analyzer/detectPackaging");
|
|
6
|
+
/**
|
|
7
|
+
* Whether any backend in this repository belongs to the product.
|
|
8
|
+
*
|
|
9
|
+
* A workspace under `docs/`, `playground/` or `examples/` that runs a server is running
|
|
10
|
+
* it to demonstrate or test something. vite has ten such workspaces, every one of them
|
|
11
|
+
* an Express server, and none of them vite.
|
|
12
|
+
*
|
|
13
|
+
* Where no workspace declares a backend but the repository does, the signal came from
|
|
14
|
+
* the root and belongs to the product: there is nowhere else for it to come from.
|
|
15
|
+
*/
|
|
16
|
+
function hasProductBackend(analysis) {
|
|
17
|
+
if (analysis.stack.backend.length === 0)
|
|
18
|
+
return false;
|
|
19
|
+
const withBackend = analysis.workspaceStacks.filter((workspace) => workspace.backend.length > 0);
|
|
20
|
+
if (withBackend.length === 0)
|
|
21
|
+
return true;
|
|
22
|
+
return withBackend.some((workspace) => !detectPackaging_1.DOCS_DIRECTORIES.test(`${workspace.root}/`));
|
|
23
|
+
}
|
|
5
24
|
function readFacts(analysis) {
|
|
6
25
|
const sourceHints = analysis.files.source.join('\n');
|
|
7
26
|
const present = (key) => analysis.detectors[key]?.present === true;
|
|
@@ -29,6 +48,7 @@ function readFacts(analysis) {
|
|
|
29
48
|
gameEngine: present('game.engine'),
|
|
30
49
|
publishable: complete('packaging.manifest'),
|
|
31
50
|
documentationSite: present('docs.site'),
|
|
51
|
+
productBackend: hasProductBackend(analysis),
|
|
32
52
|
entrypoints: present('packaging.entrypoints'),
|
|
33
53
|
packagedLicense: present('packaging.license'),
|
|
34
54
|
tests: present('quality.tests'),
|
|
@@ -195,7 +215,7 @@ const RULES = [
|
|
|
195
215
|
admissible: (f) => f.publishable
|
|
196
216
|
&& f.entrypoints
|
|
197
217
|
&& (!f.frontend || f.documentationSite)
|
|
198
|
-
&& !f.
|
|
218
|
+
&& !f.productBackend
|
|
199
219
|
&& f.mobilePlatforms.length === 0,
|
|
200
220
|
signals: [
|
|
201
221
|
{ identifies: true, label: 'a manifest that names and versions it', weight: 3, holds: (f) => f.publishable },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.1",
|
|
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": {
|