@produtype/core 0.94.0 → 0.95.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.
@@ -176,10 +176,22 @@ async function detectBackend(ctx) {
176
176
  value: ctx.dotnetWebSdk ? 'a project declaring Microsoft.NET.Sdk.Web' : 'an ASP.NET Core package reference',
177
177
  });
178
178
  }
179
- else if (ctx.dotnetDeps.length > 0 || ctx.files.all.some((f) => /\.csproj$/i.test(f))) {
180
- frameworks.push('dotnet');
181
- evidence.push({ type: 'note', value: 'a .NET project with no web SDK' });
182
- }
179
+ /**
180
+ * A `.csproj` with no web SDK used to be listed as a backend called "dotnet", and
181
+ * that is the same mistake the Rust fallback made before 0.71.0: a project file is
182
+ * not a server.
183
+ *
184
+ * .NET builds libraries, console tools, desktop applications and web services from
185
+ * the same project format, and `Microsoft.NET.Sdk.Web` is the line that says which.
186
+ * Moq — a mocking library, 243 C# files — was reported with "Backend: dotnet",
187
+ * which made it a product with a server, which kept it out of the `library` profile
188
+ * and got it judged as a client application: asked at `high` for state durability,
189
+ * asset delivery and browser crash reporting.
190
+ *
191
+ * Nothing replaces it. The language is already named in the reading-depth line and
192
+ * in the package manager, and "this is a .NET project" was never an answer to
193
+ * "what serves the requests".
194
+ */
183
195
  /**
184
196
  * Astro, which is a backend only when it is configured to be one.
185
197
  *
@@ -59,7 +59,17 @@ const DOCS_GENERATORS = [
59
59
  * of these.
60
60
  */
61
61
  exports.DOCS_DIRECTORIES = /(^|\/)(docs?|website|playground|examples?|demo|www)\//i;
62
- const FRONTEND_FILE = /\.(tsx|jsx|vue|svelte|astro)$/;
62
+ /**
63
+ * `.html` belongs here because the front-end fact counts it.
64
+ *
65
+ * The front-end detector falls back to "a page is a front end" and counts `.html`
66
+ * files, so a library whose only pages are its documentation comes out with a front
67
+ * end. This test — is every front-end file under a docs directory — looked at
68
+ * framework extensions only, so it could not see those pages and could not answer
69
+ * yes. Moq's documentation is plain HTML under `docs/`, and Moq was profiled as a
70
+ * static site: a mocking library with 243 C# files, judged on its Jekyll theme.
71
+ */
72
+ const FRONTEND_FILE = /\.(tsx|jsx|vue|svelte|astro|html?)$/;
63
73
  /**
64
74
  * The published package inside a workspace, if there is one.
65
75
  *
@@ -173,7 +183,8 @@ async function detectPackaging(ctx) {
173
183
  }
174
184
  }
175
185
  const hasManifest = ctx.packageJson !== null
176
- || all.some((f) => /(^|\/)(pyproject\.toml|setup\.py|go\.mod|Cargo\.toml|composer\.json|\w+\.gemspec)$/i.test(f));
186
+ || all.some((f) => /(^|\/)(pyproject\.toml|setup\.py|go\.mod|Cargo\.toml|composer\.json|\w+\.gemspec)$/i.test(f))
187
+ || all.some((f) => /\.(csproj|fsproj|vbproj)$/i.test(f));
177
188
  /**
178
189
  * A name and a version. Without them nothing can depend on this, whatever else it
179
190
  * does right.
@@ -181,12 +192,50 @@ async function detectPackaging(ctx) {
181
192
  const named = typeof pkg.name === 'string' && typeof pkg.version === 'string';
182
193
  const described = typeof pkg.description === 'string' && pkg.description.length > 0;
183
194
  const sourced = pkg.repository !== undefined || pkg.homepage !== undefined;
184
- const entrypointCount = nodeEntrypoints.length + (pythonEntrypoints ? 1 : 0);
195
+ /**
196
+ * The other ecosystems' way of saying "something else may depend on this".
197
+ *
198
+ * Both facts were read from `package.json` and Python packaging alone, so no
199
+ * library written in anything else could reach the `library` profile. Moq — a
200
+ * mocking library of 243 C# files, `<PackageId>Moq</PackageId>` and
201
+ * `<IsPackable>True</IsPackable>` in its csproj — was judged a client application
202
+ * and asked at `high` for state durability, asset delivery and browser crash
203
+ * reporting. tokio could not be classified at all.
204
+ *
205
+ * Each is the declaration its own toolchain requires to publish: a csproj that
206
+ * names a package id, a Cargo manifest with a `[package]` name and version beside a
207
+ * `src/lib.rs`. A crate that says `publish = false` is saying the opposite, and is
208
+ * not counted.
209
+ */
210
+ let dotnetPackageId = false;
211
+ for (const file of all.filter((f) => /\.(csproj|fsproj|vbproj)$/i.test(f)).slice(0, 12)) {
212
+ const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(ctx.root, file)) ?? '';
213
+ if (/<PackageId>|<IsPackable>\s*true\s*<\/IsPackable>|<GeneratePackageOnBuild>\s*true/i.test(raw)) {
214
+ dotnetPackageId = true;
215
+ }
216
+ }
217
+ let rustCrate = false;
218
+ for (const file of all.filter((f) => /(^|\/)Cargo\.toml$/.test(f)).slice(0, 8)) {
219
+ const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(ctx.root, file)) ?? '';
220
+ if (!/^\s*\[package\]/m.test(raw))
221
+ continue;
222
+ if (/^\s*publish\s*=\s*false/m.test(raw))
223
+ continue;
224
+ if (!/^\s*name\s*=/m.test(raw) || !/^\s*version\s*=/m.test(raw))
225
+ continue;
226
+ const crateRoot = file.replace(/Cargo\.toml$/, '');
227
+ if (all.some((f) => f === `${crateRoot}src/lib.rs`))
228
+ rustCrate = true;
229
+ }
230
+ const entrypointCount = nodeEntrypoints.length
231
+ + (pythonEntrypoints ? 1 : 0)
232
+ + (dotnetPackageId ? 1 : 0)
233
+ + (rustCrate ? 1 : 0);
185
234
  return [
186
235
  {
187
236
  key: 'packaging.manifest',
188
237
  present: hasManifest,
189
- complete: hasManifest && (named || pythonEntrypoints),
238
+ complete: hasManifest && (named || pythonEntrypoints || dotnetPackageId || rustCrate),
190
239
  evidence: hasManifest
191
240
  ? [
192
241
  publishedMember
@@ -203,9 +252,11 @@ async function detectPackaging(ctx) {
203
252
  evidence: [
204
253
  ...nodeEntrypoints.map((key) => ({ type: 'note', value: `package.json declares "${key}"` })),
205
254
  ...(pythonEntrypoints ? [{ type: 'note', value: 'a Python package or console script declaration' }] : []),
255
+ ...(dotnetPackageId ? [{ type: 'note', value: 'a project that declares a NuGet package id' }] : []),
256
+ ...(rustCrate ? [{ type: 'note', value: 'a Cargo package with a library crate root' }] : []),
206
257
  ...(hasTypes ? [{ type: 'note', value: 'TypeScript types are declared' }] : []),
207
258
  ],
208
- details: { nodeEntrypoints, hasTypes, pythonEntrypoints },
259
+ details: { nodeEntrypoints, hasTypes, pythonEntrypoints, dotnetPackageId, rustCrate },
209
260
  },
210
261
  {
211
262
  key: 'packaging.license',
@@ -161,11 +161,27 @@ function buildReport(analysis, options) {
161
161
  // and for the same reason must count. A Unity game and a Phaser game were each
162
162
  // identified as games by name and then called unreadable in the same report.
163
163
  const gameEngineDetected = analysis.detectors['game.engine']?.present === true;
164
+ /**
165
+ * A library is not an unidentified repository.
166
+ *
167
+ * The three lists describe an application, and a package has none of them, so
168
+ * "nothing identified" was true of every library — and it is what makes a report
169
+ * inconclusive. The `dotnet-library` fixture crossed that line the moment the "a
170
+ * .csproj is a backend" fallback was removed in the same release: a library with a
171
+ * NuGet manifest, read correctly, reported as a repository this analyzer could not
172
+ * make sense of.
173
+ *
174
+ * A package manager this analyzer parsed, beside a language it read, identifies a
175
+ * repository as surely as a web framework does. Loose scripts with no manifest
176
+ * still answer no.
177
+ */
178
+ const packagedInAKnownEcosystem = analysis.stack.packageManager !== 'unknown' && analysis.stack.languages.length > 0;
164
179
  const stackDetected = analysis.stack.frontend.length > 0
165
180
  || analysis.stack.backend.length > 0
166
181
  || analysis.stack.databases.length > 0
167
182
  || mobileDetected
168
- || gameEngineDetected;
183
+ || gameEngineDetected
184
+ || packagedInAKnownEcosystem;
169
185
  /**
170
186
  * Coverage counts the expectations too, not only the observed rules.
171
187
  *
@@ -169,7 +169,26 @@ exports.rules = [
169
169
  category: 'stack',
170
170
  severity: 'low',
171
171
  evaluate: ({ analysis }) => {
172
- const hasStack = analysis.stack.frontend.length > 0 || analysis.stack.backend.length > 0 || analysis.stack.databases.length > 0;
172
+ /**
173
+ * A library has no front end, no backend and no database, and it is not
174
+ * unidentifiable.
175
+ *
176
+ * The three lists are what an application is made of, and asking only about
177
+ * them made every library "generic or unknown to ProdKit" — which on tokio, a
178
+ * repository of 505 Rust files with a Cargo manifest, was enough to leave the
179
+ * whole report inconclusive. Removing the "a .csproj is a backend" fallback in
180
+ * the same release made the `dotnet-library` fixture do the same, which is how
181
+ * this surfaced.
182
+ *
183
+ * What this repository is built with is a fingerprint too: a package manager
184
+ * this analyzer actually parsed, and a language it actually read. A directory
185
+ * of loose scripts with no manifest still answers unknown.
186
+ */
187
+ const named = analysis.stack.packageManager !== 'unknown' && analysis.stack.languages.length > 0;
188
+ const hasStack = analysis.stack.frontend.length > 0
189
+ || analysis.stack.backend.length > 0
190
+ || analysis.stack.databases.length > 0
191
+ || named;
173
192
  const status = hasStack ? 'passed' : 'unknown';
174
193
  return mkFinding({
175
194
  id: 'stack.detected',
@@ -177,14 +196,31 @@ exports.rules = [
177
196
  category: 'stack',
178
197
  status,
179
198
  severity: sevForStatus(status, 'low'),
180
- description: hasStack ? 'ProdKit identified known stack signals.' : 'Stack is generic or unknown to ProdKit.',
199
+ description: hasStack
200
+ ? 'ProdKit identified known stack signals.'
201
+ : 'Stack is generic or unknown to ProdKit.',
181
202
  recommendation: hasStack
182
203
  ? 'No action required.'
183
204
  : 'Add explicit framework manifests or keep this as generic app baseline.',
205
+ /**
206
+ * A finding that passes has to point at something.
207
+ *
208
+ * When the three stack lists are empty the arrays below are empty too, so
209
+ * letting a package manager satisfy this check produced twenty passing
210
+ * findings with nothing under them — which is the one thing
211
+ * `everyFindingSaysWhatItLookedFor` exists to stop. The manifest and the
212
+ * language are what was read, so they are what is cited.
213
+ */
184
214
  evidence: [
185
215
  ...analysis.detectors['stack.frontend']?.evidence ?? [],
186
216
  ...analysis.detectors['stack.backend']?.evidence ?? [],
187
217
  ...analysis.detectors['stack.database']?.evidence ?? [],
218
+ ...(named
219
+ ? [{
220
+ type: 'note',
221
+ value: `a ${analysis.stack.packageManager} manifest and ${analysis.stack.languages.join(', ')} sources`,
222
+ }]
223
+ : []),
188
224
  ],
189
225
  });
190
226
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.94.0",
3
+ "version": "0.95.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": {