@produtype/core 0.30.0 → 0.31.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 +4 -0
- package/dist/analyzer/detectBackend.js +14 -2
- package/dist/analyzer/detectContext.d.ts +16 -0
- package/dist/analyzer/detectContext.js +10 -0
- package/dist/analyzer/detectPackaging.js +56 -0
- package/dist/expectations/profileSignals.d.ts +8 -0
- package/dist/expectations/profileSignals.js +13 -1
- package/package.json +1 -1
|
@@ -556,9 +556,12 @@ async function analyzeProject(projectPath) {
|
|
|
556
556
|
}
|
|
557
557
|
}
|
|
558
558
|
const npmDeps = {};
|
|
559
|
+
const runtimeNpmDeps = {};
|
|
559
560
|
for (const workspace of workspaces) {
|
|
560
561
|
mergeDeps(npmDeps, workspace.packageJson?.dependencies);
|
|
561
562
|
mergeDeps(npmDeps, workspace.packageJson?.devDependencies);
|
|
563
|
+
// Kept apart for one question only: whether this repository serves requests.
|
|
564
|
+
mergeDeps(runtimeNpmDeps, workspace.packageJson?.dependencies);
|
|
562
565
|
}
|
|
563
566
|
/**
|
|
564
567
|
* What the code imports, when nothing declared it.
|
|
@@ -606,6 +609,7 @@ async function analyzeProject(projectPath) {
|
|
|
606
609
|
const ctx = {
|
|
607
610
|
root,
|
|
608
611
|
files: { all: allFiles, source: sourceFiles, config: configFiles, unreadable: unreadableLanguages(allFiles) },
|
|
612
|
+
runtimeNpmDeps,
|
|
609
613
|
packageJson,
|
|
610
614
|
pythonDeps,
|
|
611
615
|
phpDeps: unique(phpDeps),
|
|
@@ -8,8 +8,20 @@ const catalogue_1 = require("./catalogue");
|
|
|
8
8
|
async function detectBackend(ctx) {
|
|
9
9
|
const frameworks = [];
|
|
10
10
|
const evidence = [];
|
|
11
|
+
/**
|
|
12
|
+
* Shipped, not merely present.
|
|
13
|
+
*
|
|
14
|
+
* axios declares `express` in devDependencies to run a test server against itself,
|
|
15
|
+
* and was read as having a backend — which kept a library out of the `library`
|
|
16
|
+
* profile. A server framework the product does not ship with is a fixture. The
|
|
17
|
+
* fallback below still reads the source, so a project that runs Express without
|
|
18
|
+
* declaring it is found anyway.
|
|
19
|
+
*
|
|
20
|
+
* Not one repository in the local corpus declares a backend framework only in
|
|
21
|
+
* devDependencies, so this changes nothing there and fixes a public library.
|
|
22
|
+
*/
|
|
11
23
|
// Express
|
|
12
|
-
if ((0, detectContext_1.
|
|
24
|
+
if ((0, detectContext_1.hasRuntimeDep)(ctx, 'express')) {
|
|
13
25
|
frameworks.push('express');
|
|
14
26
|
evidence.push({ type: 'dependency', value: 'express' });
|
|
15
27
|
}
|
|
@@ -30,7 +42,7 @@ async function detectBackend(ctx) {
|
|
|
30
42
|
// frontend-only meant an application with server routes, sessions and database
|
|
31
43
|
// access was scored as though it had none of them.
|
|
32
44
|
for (const [framework, dep] of catalogue_1.NODE_BACKEND_FRAMEWORKS) {
|
|
33
|
-
if ((0, detectContext_1.
|
|
45
|
+
if ((0, detectContext_1.hasRuntimeDep)(ctx, dep)) {
|
|
34
46
|
frameworks.push(framework);
|
|
35
47
|
evidence.push({ type: 'dependency', value: dep });
|
|
36
48
|
}
|
|
@@ -38,9 +38,25 @@ export interface DetectContext {
|
|
|
38
38
|
swiftDeps: string[];
|
|
39
39
|
/** Lowercased combined dependency map (deps + devDeps). */
|
|
40
40
|
npmDeps: Record<string, string>;
|
|
41
|
+
/**
|
|
42
|
+
* Dependencies the product ships with, without the ones it only builds and tests with.
|
|
43
|
+
*
|
|
44
|
+
* `npmDeps` merges both, which is right for almost every question — React in
|
|
45
|
+
* devDependencies still means React — and wrong for one: a server framework. axios
|
|
46
|
+
* declares `express` in devDependencies to run a test server, and was read as having
|
|
47
|
+
* a backend, which kept it out of the `library` profile it plainly belongs to.
|
|
48
|
+
*/
|
|
49
|
+
runtimeNpmDeps: Record<string, string>;
|
|
41
50
|
workspaces: WorkspaceManifest[];
|
|
42
51
|
}
|
|
43
52
|
export declare function hasDep(ctx: DetectContext, name: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Declared as something the product runs on, not merely something present.
|
|
55
|
+
*
|
|
56
|
+
* Used for server frameworks only. A test server in devDependencies is a fixture; the
|
|
57
|
+
* question "does this repository serve requests" is answered by what it ships.
|
|
58
|
+
*/
|
|
59
|
+
export declare function hasRuntimeDep(ctx: DetectContext, name: string): boolean;
|
|
44
60
|
export declare function hasAnyDep(ctx: DetectContext, names: string[]): string[];
|
|
45
61
|
export declare function hasPyDep(ctx: DetectContext, name: string): boolean;
|
|
46
62
|
export declare function hasAnyPyDep(ctx: DetectContext, names: string[]): string[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.hasDep = hasDep;
|
|
4
|
+
exports.hasRuntimeDep = hasRuntimeDep;
|
|
4
5
|
exports.hasAnyDep = hasAnyDep;
|
|
5
6
|
exports.hasPyDep = hasPyDep;
|
|
6
7
|
exports.hasAnyPyDep = hasAnyPyDep;
|
|
@@ -15,6 +16,15 @@ exports.hasAnyDotnetDep = hasAnyDotnetDep;
|
|
|
15
16
|
function hasDep(ctx, name) {
|
|
16
17
|
return Object.prototype.hasOwnProperty.call(ctx.npmDeps, name.toLowerCase());
|
|
17
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Declared as something the product runs on, not merely something present.
|
|
21
|
+
*
|
|
22
|
+
* Used for server frameworks only. A test server in devDependencies is a fixture; the
|
|
23
|
+
* question "does this repository serve requests" is answered by what it ships.
|
|
24
|
+
*/
|
|
25
|
+
function hasRuntimeDep(ctx, name) {
|
|
26
|
+
return Object.prototype.hasOwnProperty.call(ctx.runtimeNpmDeps, name.toLowerCase());
|
|
27
|
+
}
|
|
18
28
|
function hasAnyDep(ctx, names) {
|
|
19
29
|
return names.filter((n) => hasDep(ctx, n));
|
|
20
30
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.detectPackaging = detectPackaging;
|
|
4
4
|
const readTextFileSafe_1 = require("../utils/readTextFileSafe");
|
|
5
|
+
const detectContext_1 = require("./detectContext");
|
|
5
6
|
/**
|
|
6
7
|
* Whether a project is fit to be installed and depended on by someone else.
|
|
7
8
|
*
|
|
@@ -30,8 +31,48 @@ const TEST_FILE = /(^|\/)(tests?|__tests__|spec)\/|\.(test|spec)\.[cm]?[jt]sx?$|
|
|
|
30
31
|
function collect(files, pattern, limit = 3) {
|
|
31
32
|
return files.filter((file) => pattern.test(file)).slice(0, limit);
|
|
32
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Generators whose whole job is to build a documentation website.
|
|
36
|
+
*
|
|
37
|
+
* A package with a docs site in its repository was being read as whatever that site is
|
|
38
|
+
* built with. zod is a validation library and came back a client application; vite came
|
|
39
|
+
* back a client application; ruff — a linter written in Rust, with every packaging
|
|
40
|
+
* signal present — came back a static site, read from its React playground. Five public
|
|
41
|
+
* libraries out of five, all for this reason.
|
|
42
|
+
*
|
|
43
|
+
* The site that documents a product is not the product.
|
|
44
|
+
*/
|
|
45
|
+
const DOCS_GENERATORS = [
|
|
46
|
+
'@docusaurus/core', 'vitepress', 'nextra', '@astrojs/starlight', 'vuepress', 'docz',
|
|
47
|
+
'docsify-cli', 'mintlify', '@11ty/eleventy', 'mkdocs', 'mkdocs-material', 'sphinx',
|
|
48
|
+
];
|
|
49
|
+
/**
|
|
50
|
+
* Where a documentation site lives when it has no generator of its own.
|
|
51
|
+
*
|
|
52
|
+
* A hand-built docs site or a playground sits in a directory named for what it is. This
|
|
53
|
+
* is only consulted for the front-end files: a `docs/` folder full of Markdown says
|
|
54
|
+
* nothing either way, and a repository whose application happens to live under `www/`
|
|
55
|
+
* is not caught because the question asked is whether *every* front-end file is in one
|
|
56
|
+
* of these.
|
|
57
|
+
*/
|
|
58
|
+
const DOCS_DIRECTORIES = /^(docs?|website|playground|examples?|demo|www)\//i;
|
|
59
|
+
const FRONTEND_FILE = /\.(tsx|jsx|vue|svelte|astro)$/;
|
|
33
60
|
async function detectPackaging(ctx) {
|
|
34
61
|
const all = ctx.files.all;
|
|
62
|
+
/**
|
|
63
|
+
* Whether the front end in this repository is its documentation rather than its product.
|
|
64
|
+
*
|
|
65
|
+
* Two ways to know, and both have to be about the front end specifically. A docs
|
|
66
|
+
* generator in the dependencies says so outright. Failing that, every front-end file
|
|
67
|
+
* living under a directory named for documentation says the same thing more quietly —
|
|
68
|
+
* and *every* matters: one component under `examples/` beside an application is an
|
|
69
|
+
* example, while an application that is entirely under `examples/` does not exist.
|
|
70
|
+
*/
|
|
71
|
+
const docsGenerators = [...(0, detectContext_1.hasAnyDep)(ctx, DOCS_GENERATORS), ...(0, detectContext_1.hasAnyPyDep)(ctx, DOCS_GENERATORS)];
|
|
72
|
+
const frontendFiles = ctx.files.source.filter((file) => FRONTEND_FILE.test(file));
|
|
73
|
+
const frontendAllInDocs = frontendFiles.length > 0
|
|
74
|
+
&& frontendFiles.every((file) => DOCS_DIRECTORIES.test(file));
|
|
75
|
+
const documentationSite = docsGenerators.length > 0 || frontendAllInDocs;
|
|
35
76
|
const licenseFiles = collect(all, LICENSE_FILE);
|
|
36
77
|
const declaredLicense = typeof ctx.packageJson?.license === 'string';
|
|
37
78
|
const licenseEvidence = [
|
|
@@ -119,6 +160,21 @@ async function detectPackaging(ctx) {
|
|
|
119
160
|
evidence: licenseEvidence,
|
|
120
161
|
details: { files: licenseFiles.length, declared: declaredLicense },
|
|
121
162
|
},
|
|
163
|
+
{
|
|
164
|
+
/**
|
|
165
|
+
* A site that documents the product, as distinct from the product.
|
|
166
|
+
*
|
|
167
|
+
* Its own detector rather than a flag on the stack, because it is evidence a
|
|
168
|
+
* reader can check: either a generator in the manifest or the directory every
|
|
169
|
+
* front-end file sits in.
|
|
170
|
+
*/
|
|
171
|
+
key: 'docs.site',
|
|
172
|
+
present: documentationSite,
|
|
173
|
+
evidence: docsGenerators.length > 0
|
|
174
|
+
? docsGenerators.map((dep) => ({ type: 'dependency', value: dep }))
|
|
175
|
+
: frontendFiles.slice(0, 5).map((file) => ({ type: 'file', value: file, file })),
|
|
176
|
+
details: { generators: docsGenerators, frontendFiles: frontendFiles.length, allUnderDocs: frontendAllInDocs },
|
|
177
|
+
},
|
|
122
178
|
{
|
|
123
179
|
key: 'docs.readme',
|
|
124
180
|
present: readmeFiles.length > 0,
|
|
@@ -49,6 +49,14 @@ export interface ProfileFacts {
|
|
|
49
49
|
gameEngine: boolean;
|
|
50
50
|
/** A manifest that names and versions this, so something else can depend on it. */
|
|
51
51
|
publishable: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* The front end in this repository is its documentation, not its product.
|
|
54
|
+
*
|
|
55
|
+
* zod, axios, vite and ruff were all read from their docs sites: five public libraries
|
|
56
|
+
* out of five, none identified as a library. The site that documents a product is not
|
|
57
|
+
* the product.
|
|
58
|
+
*/
|
|
59
|
+
documentationSite: boolean;
|
|
52
60
|
/** Somewhere for a consumer to import: main, module, exports, bin, a console script. */
|
|
53
61
|
entrypoints: boolean;
|
|
54
62
|
packagedLicense: boolean;
|
|
@@ -28,6 +28,7 @@ function readFacts(analysis) {
|
|
|
28
28
|
containerised: present('deployment.docker'),
|
|
29
29
|
gameEngine: present('game.engine'),
|
|
30
30
|
publishable: complete('packaging.manifest'),
|
|
31
|
+
documentationSite: present('docs.site'),
|
|
31
32
|
entrypoints: present('packaging.entrypoints'),
|
|
32
33
|
packagedLicense: present('packaging.license'),
|
|
33
34
|
tests: present('quality.tests'),
|
|
@@ -184,7 +185,18 @@ const RULES = [
|
|
|
184
185
|
* project serves requests, and a package is not served.
|
|
185
186
|
*/
|
|
186
187
|
profile: 'library',
|
|
187
|
-
|
|
188
|
+
/**
|
|
189
|
+
* A front end that is documentation does not make a package an application.
|
|
190
|
+
*
|
|
191
|
+
* The gate used to be "no front end at all", which is right for a plain package and
|
|
192
|
+
* wrong for every library with a docs site — which is to say, every library anybody
|
|
193
|
+
* has heard of. A backend still disqualifies: a package is not served.
|
|
194
|
+
*/
|
|
195
|
+
admissible: (f) => f.publishable
|
|
196
|
+
&& f.entrypoints
|
|
197
|
+
&& (!f.frontend || f.documentationSite)
|
|
198
|
+
&& !f.backend
|
|
199
|
+
&& f.mobilePlatforms.length === 0,
|
|
188
200
|
signals: [
|
|
189
201
|
{ identifies: true, label: 'a manifest that names and versions it', weight: 3, holds: (f) => f.publishable },
|
|
190
202
|
{ identifies: true, label: 'an entry point for something else to import', weight: 2, holds: (f) => f.entrypoints },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.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": {
|