@produtype/core 0.49.0 → 0.50.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/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, 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
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, Axum, Actix Web, Rocket, Warp, Tide, Poem, Salvo, Tower HTTP, 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
@@ -435,6 +435,43 @@ async function analyzeProject(projectPath) {
435
435
  goDeps.push(match[1].toLowerCase());
436
436
  }
437
437
  }
438
+ /**
439
+ * Cargo.toml, read for the crates a Rust project depends on.
440
+ *
441
+ * windmill's backend is 547 Rust files and the report said "Backend: go", on the
442
+ * strength of one `go.mod` belonging to a client SDK. Rust was not being read at
443
+ * all — a whole ecosystem invisible, so a Rust web service could only ever be
444
+ * reported as whatever else happened to be lying around.
445
+ *
446
+ * The same hand-written reader as the others: `[dependencies]` and its variants open
447
+ * a block, and each entry is a crate name before `=`. A version table written as
448
+ * `[dependencies.axum]` names the crate in the heading instead, so both shapes are
449
+ * read.
450
+ */
451
+ const rustDeps = [];
452
+ for (const file of allFiles.filter((f) => /(^|\/)Cargo\.toml$/.test(f))) {
453
+ const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
454
+ let inDeps = false;
455
+ for (const line of raw.split('\n')) {
456
+ const heading = /^\s*\[([^\]]+)\]/.exec(line);
457
+ if (heading) {
458
+ const section = heading[1].trim();
459
+ const nested = /^(?:[a-z-]+\.)?(?:dependencies|dev-dependencies|build-dependencies)\.(.+)$/.exec(section);
460
+ if (nested) {
461
+ rustDeps.push(nested[1].trim().toLowerCase());
462
+ inDeps = false;
463
+ continue;
464
+ }
465
+ inDeps = /(^|\.)(dependencies|dev-dependencies|build-dependencies)$/.test(section);
466
+ continue;
467
+ }
468
+ if (!inDeps)
469
+ continue;
470
+ const entry = /^\s*([A-Za-z0-9_-]+)\s*=/.exec(line);
471
+ if (entry)
472
+ rustDeps.push(entry[1].toLowerCase());
473
+ }
474
+ }
438
475
  /**
439
476
  * pubspec.yaml, read for its two dependency blocks.
440
477
  *
@@ -664,6 +701,7 @@ async function analyzeProject(projectPath) {
664
701
  pythonDeps,
665
702
  phpDeps: unique(phpDeps),
666
703
  goDeps: unique(goDeps),
704
+ rustDeps: unique(rustDeps),
667
705
  rubyDeps: unique(rubyDeps),
668
706
  dotnetDeps: unique(dotnetDeps),
669
707
  dotnetWebSdk,
@@ -17,6 +17,14 @@
17
17
  export declare const NODE_BACKEND_FRAMEWORKS: Array<[string, string]>;
18
18
  /** Go frameworks, read from go.mod. */
19
19
  export declare const GO_BACKEND_FRAMEWORKS: Array<[string, string[]]>;
20
+ /**
21
+ * Rust frameworks, read from Cargo.toml.
22
+ *
23
+ * windmill serves its requests from 547 Rust files and was reported as a Go backend,
24
+ * because a client SDK in the same repository carries a go.mod and Rust was not read
25
+ * at all.
26
+ */
27
+ export declare const RUST_BACKEND_FRAMEWORKS: Array<[string, string[]]>;
20
28
  /** Ruby frameworks, read from the Gemfile. */
21
29
  export declare const RUBY_BACKEND_FRAMEWORKS: Array<[string, string[]]>;
22
30
  /** PHP frameworks, read from composer.json. */
@@ -15,7 +15,7 @@
15
15
  * that loses users.
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.LANGUAGES = exports.FRONTEND_FRAMEWORKS = exports.PYTHON_BACKEND_FRAMEWORKS = exports.PHP_BACKEND_FRAMEWORKS = exports.RUBY_BACKEND_FRAMEWORKS = exports.GO_BACKEND_FRAMEWORKS = exports.NODE_BACKEND_FRAMEWORKS = void 0;
18
+ exports.LANGUAGES = exports.FRONTEND_FRAMEWORKS = exports.PYTHON_BACKEND_FRAMEWORKS = exports.PHP_BACKEND_FRAMEWORKS = exports.RUBY_BACKEND_FRAMEWORKS = exports.RUST_BACKEND_FRAMEWORKS = exports.GO_BACKEND_FRAMEWORKS = exports.NODE_BACKEND_FRAMEWORKS = void 0;
19
19
  exports.labelFor = labelFor;
20
20
  exports.hasLabel = hasLabel;
21
21
  exports.supportedStacks = supportedStacks;
@@ -43,6 +43,23 @@ exports.GO_BACKEND_FRAMEWORKS = [
43
43
  ['gorilla', ['gorilla/mux']],
44
44
  ['beego', ['beego/beego']],
45
45
  ];
46
+ /**
47
+ * Rust frameworks, read from Cargo.toml.
48
+ *
49
+ * windmill serves its requests from 547 Rust files and was reported as a Go backend,
50
+ * because a client SDK in the same repository carries a go.mod and Rust was not read
51
+ * at all.
52
+ */
53
+ exports.RUST_BACKEND_FRAMEWORKS = [
54
+ ['axum', ['axum']],
55
+ ['actix-web', ['actix-web']],
56
+ ['rocket', ['rocket']],
57
+ ['warp', ['warp']],
58
+ ['tide', ['tide']],
59
+ ['poem', ['poem']],
60
+ ['salvo', ['salvo']],
61
+ ['tower-http', ['tower-http']],
62
+ ];
46
63
  /** Ruby frameworks, read from the Gemfile. */
47
64
  exports.RUBY_BACKEND_FRAMEWORKS = [
48
65
  ['rails', ['rails']],
@@ -138,6 +155,15 @@ const LABELS = {
138
155
  chi: 'chi',
139
156
  gorilla: 'Gorilla',
140
157
  beego: 'Beego',
158
+ axum: 'Axum',
159
+ 'actix-web': 'Actix Web',
160
+ rocket: 'Rocket',
161
+ warp: 'Warp',
162
+ tide: 'Tide',
163
+ poem: 'Poem',
164
+ salvo: 'Salvo',
165
+ 'tower-http': 'Tower HTTP',
166
+ rust: 'Rust',
141
167
  rails: 'Rails',
142
168
  sinatra: 'Sinatra',
143
169
  hanami: 'Hanami',
@@ -260,6 +286,7 @@ function supportedStacks() {
260
286
  ...entries(exports.PYTHON_BACKEND_FRAMEWORKS.map(([id]) => id)),
261
287
  ...entries(exports.GO_BACKEND_FRAMEWORKS.map(([id]) => id)),
262
288
  { id: 'go', label: 'Go', detectedFrom: 'a go.mod with no framework in it — net/http is a real answer' },
289
+ ...entries(exports.RUST_BACKEND_FRAMEWORKS.map(([id]) => id)),
263
290
  ...entries(exports.RUBY_BACKEND_FRAMEWORKS.map(([id]) => id)),
264
291
  { id: 'ruby', label: 'Ruby', detectedFrom: 'a Gemfile with no web framework in it' },
265
292
  ...entries(exports.PHP_BACKEND_FRAMEWORKS.map(([id]) => id)),
@@ -5,6 +5,30 @@ const detectContext_1 = require("./detectContext");
5
5
  const textSearch_1 = require("../utils/textSearch");
6
6
  const readTextFileSafe_1 = require("../utils/readTextFileSafe");
7
7
  const catalogue_1 = require("./catalogue");
8
+ /**
9
+ * How much of the source is written in one language.
10
+ *
11
+ * A manifest says a language is present; a share says it is what the product is made
12
+ * of. The mobile detector has drawn this distinction since a single MAUI client
13
+ * decided the profile of a nine-project .NET solution.
14
+ */
15
+ function languageShare(ctx, extension) {
16
+ const source = ctx.files.source;
17
+ if (source.length === 0)
18
+ return 0;
19
+ return source.filter((file) => extension.test(file)).length / source.length;
20
+ }
21
+ /**
22
+ * Below this a language is present in the repository without being what serves the
23
+ * requests.
24
+ *
25
+ * windmill is the measurement: two Go files beside 547 Rust ones, 0.05% of its source,
26
+ * and the report said "Backend: go". The line is not tuned to that case — anything
27
+ * under one file in twenty is a client, a script or a sample, and every backend in the
28
+ * verification corpus is far above it. A repository genuinely split between two server
29
+ * languages reports both, which is the right answer for one.
30
+ */
31
+ const MINIMUM_BACKEND_SHARE = 0.05;
8
32
  async function detectBackend(ctx) {
9
33
  const frameworks = [];
10
34
  const evidence = [];
@@ -63,10 +87,37 @@ async function detectBackend(ctx) {
63
87
  for (const dep of hits)
64
88
  evidence.push({ type: 'dependency', value: dep });
65
89
  }
66
- if (!namedGoFramework && ctx.files.all.some((f) => /(^|\/)go\.mod$/.test(f))) {
90
+ /**
91
+ * A go.mod is not a Go backend on its own.
92
+ *
93
+ * windmill carries one for a client SDK beside 547 Rust files, and the report said
94
+ * "Backend: go". The same share test the mobile detector already uses: a language
95
+ * has to be a real part of what is written here before it names the backend.
96
+ */
97
+ const goShare = languageShare(ctx, /\.go$/);
98
+ if (!namedGoFramework && goShare >= MINIMUM_BACKEND_SHARE && ctx.files.all.some((f) => /(^|\/)go\.mod$/.test(f))) {
67
99
  frameworks.push('go');
68
100
  evidence.push({ type: 'note', value: 'a Go module with no web framework named in go.mod' });
69
101
  }
102
+ /** Rust, read from Cargo.toml. */
103
+ let namedRustFramework = false;
104
+ for (const [framework, deps] of catalogue_1.RUST_BACKEND_FRAMEWORKS) {
105
+ const hits = (0, detectContext_1.hasAnyRustDep)(ctx, deps);
106
+ if (!hits.length)
107
+ continue;
108
+ namedRustFramework = true;
109
+ frameworks.push(framework);
110
+ for (const dep of hits)
111
+ evidence.push({ type: 'dependency', value: dep });
112
+ }
113
+ // Same reasoning as Go: a crate that serves requests from the standard library and a
114
+ // hand-rolled loop is still a backend, and a Cargo.toml alone is not.
115
+ if (!namedRustFramework
116
+ && languageShare(ctx, /\.rs$/) >= MINIMUM_BACKEND_SHARE
117
+ && ctx.files.all.some((f) => /(^|\/)Cargo\.toml$/.test(f))) {
118
+ frameworks.push('rust');
119
+ evidence.push({ type: 'note', value: 'a Cargo manifest with no web framework named in it' });
120
+ }
70
121
  /** Ruby, read from the Gemfile. */
71
122
  let namedRubyFramework = false;
72
123
  for (const [framework, deps] of catalogue_1.RUBY_BACKEND_FRAMEWORKS) {
@@ -26,6 +26,8 @@ export interface DetectContext {
26
26
  phpDeps: string[];
27
27
  /** Module paths from go.mod, lowercase. */
28
28
  goDeps: string[];
29
+ /** Crate names from Cargo.toml, lowercase. */
30
+ rustDeps: string[];
29
31
  /** Gem names from the Gemfile, lowercase. */
30
32
  rubyDeps: string[];
31
33
  /** PackageReference and FrameworkReference names from .csproj, lowercase. */
@@ -71,6 +73,7 @@ export declare function hasRuntimeDep(ctx: DetectContext, name: string): boolean
71
73
  export declare function hasRuntimePyDep(ctx: DetectContext, name: string): boolean;
72
74
  export declare function hasAnyDep(ctx: DetectContext, names: string[]): string[];
73
75
  export declare function hasPyDep(ctx: DetectContext, name: string): boolean;
76
+ export declare function hasAnyRustDep(ctx: DetectContext, names: string[]): string[];
74
77
  export declare function hasAnyPyDep(ctx: DetectContext, names: string[]): string[];
75
78
  export declare function hasPhpDep(ctx: DetectContext, name: string): boolean;
76
79
  export declare function hasAnyPhpDep(ctx: DetectContext, names: string[]): string[];
@@ -5,6 +5,7 @@ exports.hasRuntimeDep = hasRuntimeDep;
5
5
  exports.hasRuntimePyDep = hasRuntimePyDep;
6
6
  exports.hasAnyDep = hasAnyDep;
7
7
  exports.hasPyDep = hasPyDep;
8
+ exports.hasAnyRustDep = hasAnyRustDep;
8
9
  exports.hasAnyPyDep = hasAnyPyDep;
9
10
  exports.hasPhpDep = hasPhpDep;
10
11
  exports.hasAnyPhpDep = hasAnyPhpDep;
@@ -36,6 +37,9 @@ function hasAnyDep(ctx, names) {
36
37
  function hasPyDep(ctx, name) {
37
38
  return ctx.pythonDeps.includes(name.toLowerCase());
38
39
  }
40
+ function hasAnyRustDep(ctx, names) {
41
+ return names.filter((name) => ctx.rustDeps.includes(name.toLowerCase()));
42
+ }
39
43
  function hasAnyPyDep(ctx, names) {
40
44
  return names.filter((n) => hasPyDep(ctx, n));
41
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.49.0",
3
+ "version": "0.50.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": {