@produtype/core 0.31.0 → 0.32.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.
@@ -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
- const DOCS_DIRECTORIES = /^(docs?|website|playground|examples?|demo|www)\//i;
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 pkg = (ctx.packageJson ?? {});
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 ? [{ type: 'note', value: named ? 'a manifest with a name and a version' : 'a package manifest' }] : [],
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.backend
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.31.0",
3
+ "version": "0.32.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": {