@mh-alikhani/bunready 0.1.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/LICENSE +21 -0
  3. package/README.md +129 -0
  4. package/action.yml +89 -0
  5. package/docs/CONFIGURATION.md +44 -0
  6. package/docs/JSON-OUTPUT.md +50 -0
  7. package/docs/RELEASING.md +65 -0
  8. package/docs/adr/0001-data-source-policy.md +36 -0
  9. package/docs/adr/0002-rule-severity-model.md +42 -0
  10. package/docs/adr/0003-release-pipeline.md +51 -0
  11. package/docs/brand/favicon.svg +8 -0
  12. package/docs/brand/guidelines.md +70 -0
  13. package/docs/brand/logo-dark.svg +11 -0
  14. package/docs/brand/logo-mono.svg +11 -0
  15. package/docs/brand/logo.svg +11 -0
  16. package/docs/brand/mark.svg +8 -0
  17. package/docs/brand/tokens.json +74 -0
  18. package/docs/demo.md +37 -0
  19. package/package.json +71 -0
  20. package/src/cli/args.ts +177 -0
  21. package/src/cli/copy.ts +76 -0
  22. package/src/cli/index.ts +5 -0
  23. package/src/cli/io.ts +20 -0
  24. package/src/cli/run.ts +98 -0
  25. package/src/cli/theme.ts +59 -0
  26. package/src/config/baseline.ts +116 -0
  27. package/src/config/config.ts +113 -0
  28. package/src/core/errors.ts +59 -0
  29. package/src/core/fs.ts +72 -0
  30. package/src/core/version.ts +9 -0
  31. package/src/report/human.ts +100 -0
  32. package/src/report/json.ts +11 -0
  33. package/src/report/sarif.ts +73 -0
  34. package/src/report/types.ts +114 -0
  35. package/src/rules/data/native-packages.json +81 -0
  36. package/src/rules/data/node-runtime.json +6 -0
  37. package/src/rules/install/engines.ts +74 -0
  38. package/src/rules/install/index.ts +27 -0
  39. package/src/rules/install/lifecycle-scripts.ts +70 -0
  40. package/src/rules/install/lockfile-presence.ts +68 -0
  41. package/src/rules/install/native-addon.ts +126 -0
  42. package/src/rules/run/index.ts +114 -0
  43. package/src/rules/runtime/builtins.ts +148 -0
  44. package/src/rules/runtime/index.ts +18 -0
  45. package/src/rules/severity.ts +46 -0
  46. package/src/scanner/execute.ts +301 -0
  47. package/src/scanner/graph.ts +77 -0
  48. package/src/scanner/lockfile.ts +545 -0
  49. package/src/scanner/manifest.ts +109 -0
  50. package/src/scanner/scan.ts +322 -0
  51. package/src/scanner/semver.ts +227 -0
  52. package/src/scanner/sources.ts +355 -0
  53. package/src/scanner/target.ts +224 -0
  54. package/src/scanner/workspaces.ts +170 -0
@@ -0,0 +1,170 @@
1
+ import { join } from "node:path";
2
+ import type { FileSystem } from "../core/fs";
3
+ import type { Manifest } from "./manifest";
4
+
5
+ /**
6
+ * Workspace discovery.
7
+ *
8
+ * A monorepo is detected from `package.json` `workspaces` (array or `packages`)
9
+ * or from `pnpm-workspace.yaml`. Globs are expanded with a deliberately small
10
+ * matcher: a literal path, one `*` level, or `**` to a bounded depth. Anything
11
+ * more exotic is reported as "nothing matched" rather than guessed at, and
12
+ * `node_modules` and build output are never walked.
13
+ */
14
+ export const WORKSPACE_EXCLUDES = [
15
+ "node_modules",
16
+ ".git",
17
+ "dist",
18
+ "build",
19
+ "coverage",
20
+ "out",
21
+ ".next",
22
+ ] as const;
23
+
24
+ const MAX_DEPTH = 3;
25
+
26
+ export interface WorkspacePackage {
27
+ /** Directory relative to the workspace root, using forward slashes. */
28
+ readonly relative: string;
29
+ }
30
+
31
+ export function workspacePatterns(manifest: Manifest, pnpmWorkspace: string | undefined): string[] {
32
+ if (manifest.workspaces.length > 0) {
33
+ return [...manifest.workspaces]
34
+ .map((pattern) => pattern.trim())
35
+ .filter((pattern) => pattern !== "");
36
+ }
37
+ return pnpmWorkspace === undefined ? [] : patternsFromPnpmWorkspace(pnpmWorkspace);
38
+ }
39
+
40
+ /** The `packages:` list of a pnpm-workspace.yaml. */
41
+ export function patternsFromPnpmWorkspace(text: string): string[] {
42
+ const patterns: string[] = [];
43
+ let inPackages = false;
44
+
45
+ for (const line of text.split(/\r?\n/)) {
46
+ const trimmed = line.trim();
47
+ if (trimmed === "" || trimmed.startsWith("#")) {
48
+ continue;
49
+ }
50
+ if (!/^\s/.test(line)) {
51
+ inPackages = /^packages:/.test(line);
52
+ continue;
53
+ }
54
+ if (!inPackages) {
55
+ continue;
56
+ }
57
+ const entry = /^-+\s*(.+?)\s*$/.exec(trimmed);
58
+ if (entry?.[1] !== undefined) {
59
+ patterns.push(entry[1].replace(/^['"]|['"]$/g, ""));
60
+ }
61
+ }
62
+
63
+ return patterns;
64
+ }
65
+
66
+ function isExcluded(name: string): boolean {
67
+ return (WORKSPACE_EXCLUDES as readonly string[]).includes(name);
68
+ }
69
+
70
+ async function hasManifest(dir: string, fs: FileSystem): Promise<boolean> {
71
+ return (await fs.readTextFile(join(dir, "package.json"))).kind === "text";
72
+ }
73
+
74
+ async function expandGlob(root: string, segments: string[], fs: FileSystem): Promise<string[]> {
75
+ let prefixes: string[] = [""];
76
+
77
+ const join = (prefix: string, name: string): string =>
78
+ prefix === "" ? name : `${prefix}/${name}`;
79
+
80
+ for (const segment of segments) {
81
+ const next: string[] = [];
82
+
83
+ if (segment === "**") {
84
+ for (const prefix of prefixes) {
85
+ next.push(prefix === "" ? "." : prefix);
86
+ let level = [prefix];
87
+ for (let depth = 0; depth < MAX_DEPTH && level.length > 0; depth += 1) {
88
+ const deeper: string[] = [];
89
+ for (const current of level) {
90
+ for (const entry of await fs.listDirectory(join(root, current))) {
91
+ if (!entry.isDirectory || isExcluded(entry.name)) {
92
+ continue;
93
+ }
94
+ const child = join(current, entry.name);
95
+ deeper.push(child);
96
+ next.push(child);
97
+ }
98
+ }
99
+ level = deeper;
100
+ }
101
+ }
102
+ } else if (segment === "*") {
103
+ for (const prefix of prefixes) {
104
+ for (const entry of await fs.listDirectory(join(root, prefix))) {
105
+ if (!entry.isDirectory || isExcluded(entry.name)) {
106
+ continue;
107
+ }
108
+ next.push(join(prefix, entry.name));
109
+ }
110
+ }
111
+ } else {
112
+ for (const prefix of prefixes) {
113
+ next.push(join(prefix, segment));
114
+ }
115
+ }
116
+
117
+ prefixes = next;
118
+ if (prefixes.length === 0) {
119
+ break;
120
+ }
121
+ }
122
+
123
+ return prefixes.filter((prefix) => prefix !== "" && prefix !== ".");
124
+ }
125
+
126
+ export async function findWorkspacePackages(
127
+ root: string,
128
+ patterns: readonly string[],
129
+ fs: FileSystem,
130
+ ): Promise<WorkspacePackage[]> {
131
+ const found = new Map<string, WorkspacePackage>();
132
+
133
+ for (const pattern of patterns) {
134
+ const cleaned = pattern.replace(/\\/g, "/").replace(/\/+$/, "").replace(/^\.\//, "");
135
+ if (cleaned === "" || cleaned === ".") {
136
+ continue;
137
+ }
138
+ const segments = cleaned.split("/").filter((segment) => segment !== "");
139
+ const candidates = segments.some((segment) => segment.includes("*"))
140
+ ? await expandGlob(root, segments, fs)
141
+ : [cleaned];
142
+
143
+ for (const candidate of candidates) {
144
+ const relative = candidate.replace(/\\/g, "/");
145
+ if (relative === "" || found.has(relative)) {
146
+ continue;
147
+ }
148
+ if (!(await hasManifest(join(root, relative), fs))) {
149
+ continue;
150
+ }
151
+ found.set(relative, { relative });
152
+ }
153
+ }
154
+
155
+ return [...found.values()].sort((a, b) => a.relative.localeCompare(b.relative));
156
+ }
157
+
158
+ export const PNPM_WORKSPACE_FILENAME = "pnpm-workspace.yaml";
159
+
160
+ /** Read and parse `pnpm-workspace.yaml`, if it is there. */
161
+ export async function readPnpmWorkspace(root: string, fs: FileSystem): Promise<string | undefined> {
162
+ const outcome = await fs.readTextFile(join(root, PNPM_WORKSPACE_FILENAME));
163
+ return outcome.kind === "text" ? outcome.text : undefined;
164
+ }
165
+
166
+ /** Keep only the packages matching a scope string, by relative path or name. */
167
+ export function scopeMatches(relative: string, scope: string): boolean {
168
+ const needle = scope.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
169
+ return relative === needle || relative.includes(needle);
170
+ }