g1t/services/projects/src/kind.test.ts

112 lines7,473 bytesCodeBlame
1import { test } from "node:test";
2import assert from "node:assert/strict";
3
4import { type KindFacts, type RootFiles, deploysSetting, detectKind, goFilesToRead, resolveKind } from "./kind.ts";
5
6const root = (text: Record<string, string>, more: Partial<RootFiles> = {}): RootFiles => ({
7 entries: [...Object.keys(text), ...(more.entries ?? [])],
8 text,
9 ...(more.src ? { src: more.src } : {}),
10 ...(more.public ? { public: more.public } : {}),
11});
12
13// psr/log's composer.json, as it is at 3.0.2: no type, an autoload.
14const PSR_LOG = JSON.stringify({
15 name: "psr/log",
16 description: "Common interface for logging libraries",
17 license: "MIT",
18 require: { php: ">=8.0.0" },
19 autoload: { "psr-4": { "Psr\\Log\\": "src" } },
20});
21
22test("a Composer package is a library; a Composer project is an app", () => {
23 assert.deepEqual(detectKind(root({ "composer.json": PSR_LOG }, { entries: ["src/", "README.md"] })), {
24 kind: "library",
25 detail: "composer.json has autoload and no public/index.php.",
26 ecosystem: "composer",
27 });
28 assert.equal(detectKind(root({ "composer.json": '{"type":"library"}' })).kind, "library");
29 assert.equal(detectKind(root({ "composer.json": '{"type":"symfony-bundle"}' })).detail, 'composer.json says "type": "symfony-bundle".');
30 assert.equal(detectKind(root({ "composer.json": '{"type":"project","autoload":{}}' })).kind, "app");
31 // Laravel and the like: no type, but a front controller to serve.
32 const laravel = root({ "composer.json": '{"autoload":{"psr-4":{}}}' }, { entries: ["public/"], public: ["index.php", ".htaccess"] });
33 assert.equal(detectKind(laravel).kind, "app");
34});
35
36test("package.json: an entry point without a start or dev script is a library", () => {
37 assert.equal(detectKind(root({ "package.json": '{"name":"x","main":"dist/index.js","scripts":{"build":"tsc"}}' })).kind, "library");
38 assert.equal(detectKind(root({ "package.json": '{"exports":{".":"./index.js"}}' })).detail, 'package.json has "exports" and no start or dev script.');
39 assert.equal(detectKind(root({ "package.json": '{"main":"index.js","scripts":{"start":"node index.js"}}' })).kind, "app");
40 assert.equal(detectKind(root({ "package.json": '{"main":"index.js","scripts":{"dev":"vite"}}' })).kind, "app");
41 assert.equal(detectKind(root({ "package.json": '{"files":["dist"],"dependencies":{"next":"15"}}' })).detail, "package.json depends on next.");
42 assert.equal(detectKind(root({ "package.json": '{"files":["dist"],"devDependencies":{"@sveltejs/kit":"2"}}' })).kind, "app");
43 // A library built with Vite stays a library; a Vite site has index.html.
44 assert.equal(detectKind(root({ "package.json": '{"main":"dist/x.js","devDependencies":{"vite":"6"}}' })).kind, "library");
45 assert.equal(detectKind(root({ "package.json": '{"main":"x.js","devDependencies":{"vite":"6"}}' }, { entries: ["index.html"] })).kind, "app");
46 // Nothing to tell from.
47 assert.equal(detectKind(root({ "package.json": '{"name":"x"}' })).kind, null);
48 assert.equal(detectKind(root({ "package.json": "not json" })).kind, null);
49});
50
51test("Cargo: a library and no binary is a library", () => {
52 assert.equal(detectKind(root({ "Cargo.toml": "[package]\nname = \"x\"\n\n[lib]\n" }, { entries: ["src/"], src: ["lib.rs"] })).kind, "library");
53 assert.equal(detectKind(root({ "Cargo.toml": "[package]\nname = \"x\"\n" }, { entries: ["src/"], src: ["lib.rs"] })).kind, "library");
54 assert.equal(detectKind(root({ "Cargo.toml": "[package]\n[lib]\n" }, { entries: ["src/"], src: ["lib.rs", "main.rs"] })).kind, "app");
55 assert.equal(detectKind(root({ "Cargo.toml": "[package]\n[lib]\n[[bin]]\nname = \"x\"\n" })).kind, "app");
56});
57
58test("Go: a module with no package main at its root is a library", () => {
59 const lib = root({ "go.mod": "module g1t.sh/acme/log\n", "log.go": "// Package log logs.\npackage log\n" });
60 assert.deepEqual(detectKind(lib), { kind: "library", detail: "go.mod, with no package main at the root.", ecosystem: "go" });
61 const app = root({ "go.mod": "module x\n", "main.go": "package main\n\nfunc main() {}\n" });
62 assert.deepEqual(detectKind(app), { kind: "app", detail: "main.go at the root is package main.", ecosystem: "go" });
63 assert.equal(detectKind(root({ "go.mod": "module x\n" }, { entries: ["cmd/"] })).kind, null);
64 assert.deepEqual(goFilesToRead(["a.go", "a_test.go", "b.go", "c.go", "d.go", "go.mod"]), ["a.go", "b.go", "c.go"]);
65});
66
67test("Python: a build backend and no app framework is a library", () => {
68 const lib = '[build-system]\nrequires = ["hatchling"]\nbuild-backend = "hatchling.build"\n[project]\ndependencies = ["httpx"]\n';
69 assert.equal(detectKind(root({ "pyproject.toml": lib })).kind, "library");
70 const app = '[build-system]\nbuild-backend = "hatchling.build"\n[project]\ndependencies = ["django>=5", "flask-login"]\n';
71 assert.equal(detectKind(root({ "pyproject.toml": app })).detail, "pyproject.toml depends on django.");
72 // A plugin named after a framework is not the framework.
73 assert.equal(detectKind(root({ "pyproject.toml": '[build-system]\nbuild-backend = "x"\ndependencies = ["flask-login"]\n' })).kind, "library");
74});
75
76test("the language's manifest decides before package.json, and a Workers config is always an app", () => {
77 const php = root({ "composer.json": PSR_LOG, "package.json": '{"scripts":{"dev":"vite"}}' });
78 assert.equal(detectKind(php).ecosystem, "composer");
79 assert.equal(detectKind(php).kind, "library");
80 assert.equal(detectKind(root({ "package.json": '{"main":"x.js"}' }, { entries: ["wrangler.jsonc"] })).kind, "app");
81 assert.equal(detectKind(root({}, { entries: ["index.html", "style.css"] })).kind, "app");
82 assert.deepEqual(detectKind(root({}, { entries: ["README.md"] })), { kind: null, detail: "No manifest at the root says what it is.", ecosystem: null });
83});
84
85const auto: KindFacts = { deploys: "auto", deploymentsOn: false, linkedPackage: null, detected: null };
86
87test("the setting wins over everything", () => {
88 assert.equal(resolveKind({ ...auto, deploys: "yes", linkedPackage: "Composer package psr/log" }).kind, "app");
89 assert.deepEqual(resolveKind({ ...auto, deploys: "no", deploymentsOn: true }), {
90 kind: "library",
91 reason: { by: "set", detail: "Set in its settings: it doesn't deploy." },
92 });
93});
94
95test("on auto: Deployments on, then packages, then files, then an app", () => {
96 const library = { kind: "library" as const, detail: "composer.json has autoload and no public/index.php." };
97 assert.equal(resolveKind({ ...auto, deploymentsOn: true, linkedPackage: "Composer package psr/log", detected: library }).reason.by, "deployments");
98 assert.deepEqual(resolveKind({ ...auto, linkedPackage: "Composer package psr/log", detected: library }), {
99 kind: "library",
100 reason: { by: "packages", detail: "Its repository publishes the Composer package psr/log." },
101 });
102 assert.deepEqual(resolveKind({ ...auto, detected: library }), { kind: "library", reason: { by: "files", ...{ detail: library.detail } } });
103 assert.equal(resolveKind({ ...auto, detected: { kind: "app", detail: "package.json has a start script." } }).reason.by, "files");
104 assert.deepEqual(resolveKind({ ...auto, deploymentsOn: null, detected: { kind: null, detail: "" } }).kind, "app");
105 assert.equal(resolveKind(auto).reason.by, "default");
106});
107
108test("an unknown setting is auto", () => {
109 assert.equal(deploysSetting("no"), "no");
110 assert.equal(deploysSetting(null), "auto");
111 assert.equal(deploysSetting("sometimes"), "auto");
112});