@doki-land/nifty 0.0.2 → 0.0.3
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 +29 -0
- package/cli/nifty.mjs +11 -11
- package/locales/en-US/cli.json +68 -64
- package/locales/locales.json +4 -4
- package/package.json +20 -16
- package/src/cli/authArgs.ts +52 -52
- package/src/cli/bump-cmd.ts +55 -55
- package/src/cli/bump.ts +237 -237
- package/src/cli/change-logs-cmd.ts +97 -97
- package/src/cli/cli.ts +47 -45
- package/src/cli/context.ts +36 -36
- package/src/cli/format-cmd.ts +23 -0
- package/src/cli/format.ts +28 -0
- package/src/cli/index.ts +9 -9
- package/src/cli/install-native-cmd.ts +22 -22
- package/src/cli/install-native.ts +80 -80
- package/src/cli/lint-cmd.ts +35 -33
- package/src/cli/lint.ts +102 -113
- package/src/cli/main.ts +1 -1
- package/src/cli/options.ts +40 -40
- package/src/cli/publish-cmd.ts +38 -34
- package/src/cli/publish.ts +95 -95
- package/src/cli/resolveNpm.ts +16 -16
- package/src/cli/retime-cmd.ts +62 -62
- package/src/cli/reword-cmd.ts +87 -87
- package/src/cli/trust-cmd.ts +38 -38
- package/src/cli/trust.ts +83 -83
- package/src/cli/update-cmd.ts +20 -20
- package/src/cli/update.ts +32 -185
- package/src/cli/upload-cmd.ts +44 -44
- package/src/cli/upload.ts +313 -313
- package/src/cli/workspace.ts +124 -124
- package/src/config/defineConfig.ts +12 -12
- package/src/config/detectProject.ts +189 -189
- package/src/config/index.ts +25 -25
- package/src/config/loadConfig.ts +129 -129
- package/src/config/types.ts +57 -57
- package/src/git.ts +61 -61
- package/src/github.ts +184 -184
- package/src/gitmoji.ts +77 -77
- package/src/index.ts +36 -36
- package/src/native.ts +665 -507
- package/src/nifty.ts +95 -95
- package/src/types.ts +40 -40
package/src/cli/workspace.ts
CHANGED
|
@@ -1,124 +1,124 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
3
|
-
|
|
4
|
-
import { detectProjectLayout } from "../config/detectProject.js";
|
|
5
|
-
|
|
6
|
-
export type WorkspacePackage = {
|
|
7
|
-
name: string;
|
|
8
|
-
version: string;
|
|
9
|
-
dir: string;
|
|
10
|
-
manifestPath: string;
|
|
11
|
-
private: boolean;
|
|
12
|
-
manifest: PackageManifest;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export type PackageManifest = {
|
|
16
|
-
name: string;
|
|
17
|
-
version: string;
|
|
18
|
-
private?: boolean;
|
|
19
|
-
dependencies?: Record<string, string>;
|
|
20
|
-
devDependencies?: Record<string, string>;
|
|
21
|
-
optionalDependencies?: Record<string, string>;
|
|
22
|
-
peerDependencies?: Record<string, string>;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const DEPENDENCY_FIELDS = [
|
|
26
|
-
"dependencies",
|
|
27
|
-
"devDependencies",
|
|
28
|
-
"optionalDependencies",
|
|
29
|
-
"peerDependencies",
|
|
30
|
-
] as const;
|
|
31
|
-
|
|
32
|
-
export function findWorkspaceRoot(cwd: string): string {
|
|
33
|
-
const layout = detectProjectLayout(cwd);
|
|
34
|
-
const candidates = [layout.root, resolve(cwd)];
|
|
35
|
-
for (const candidate of candidates) {
|
|
36
|
-
const crates = join(candidate, "projects", "crates");
|
|
37
|
-
const packages = join(candidate, "projects", "packages");
|
|
38
|
-
if (existsSync(crates) && existsSync(packages)) {
|
|
39
|
-
return candidate;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
throw new Error("could not find Nifty workspace (expected projects/crates and projects/packages)");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function listWorkspacePackages(root: string): WorkspacePackage[] {
|
|
46
|
-
const packagesDir = join(root, "projects", "packages");
|
|
47
|
-
if (!existsSync(packagesDir)) {
|
|
48
|
-
return [];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const packages: WorkspacePackage[] = [];
|
|
52
|
-
for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
|
|
53
|
-
if (!entry.isDirectory() || entry.name === "node_modules") {
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
const manifestPath = join(packagesDir, entry.name, "package.json");
|
|
57
|
-
if (!existsSync(manifestPath)) {
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as PackageManifest;
|
|
61
|
-
if (!manifest.name || !manifest.version) {
|
|
62
|
-
throw new Error(`package.json missing name or version: ${manifestPath}`);
|
|
63
|
-
}
|
|
64
|
-
packages.push({
|
|
65
|
-
name: manifest.name,
|
|
66
|
-
version: manifest.version,
|
|
67
|
-
dir: join(packagesDir, entry.name),
|
|
68
|
-
manifestPath,
|
|
69
|
-
private: manifest.private === true,
|
|
70
|
-
manifest,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return packages.sort((a, b) => a.name.localeCompare(b.name));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function collectInternalDependencyNames(
|
|
78
|
-
pkg: WorkspacePackage,
|
|
79
|
-
byName: Map<string, WorkspacePackage>,
|
|
80
|
-
): Set<string> {
|
|
81
|
-
const deps = new Set<string>();
|
|
82
|
-
for (const field of DEPENDENCY_FIELDS) {
|
|
83
|
-
const entries = pkg.manifest[field];
|
|
84
|
-
if (!entries) {
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
for (const [depName, spec] of Object.entries(entries)) {
|
|
88
|
-
if (byName.has(depName)) {
|
|
89
|
-
deps.add(depName);
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
const resolved = resolveWorkspaceDependencyName(spec, pkg.dir, byName);
|
|
93
|
-
if (resolved) {
|
|
94
|
-
deps.add(resolved);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return deps;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function resolveWorkspaceDependencyName(
|
|
102
|
-
spec: string,
|
|
103
|
-
packageDir: string,
|
|
104
|
-
byName: Map<string, WorkspacePackage>,
|
|
105
|
-
): string | undefined {
|
|
106
|
-
if (spec.startsWith("workspace:")) {
|
|
107
|
-
return undefined;
|
|
108
|
-
}
|
|
109
|
-
if (!spec.startsWith("file:")) {
|
|
110
|
-
return undefined;
|
|
111
|
-
}
|
|
112
|
-
const targetDir = resolve(packageDir, spec.slice("file:".length));
|
|
113
|
-
for (const candidate of byName.values()) {
|
|
114
|
-
if (candidate.dir === targetDir) {
|
|
115
|
-
return candidate.name;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
const manifestPath = join(targetDir, "package.json");
|
|
119
|
-
if (!existsSync(manifestPath)) {
|
|
120
|
-
return undefined;
|
|
121
|
-
}
|
|
122
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as PackageManifest;
|
|
123
|
-
return manifest.name;
|
|
124
|
-
}
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { detectProjectLayout } from "../config/detectProject.js";
|
|
5
|
+
|
|
6
|
+
export type WorkspacePackage = {
|
|
7
|
+
name: string;
|
|
8
|
+
version: string;
|
|
9
|
+
dir: string;
|
|
10
|
+
manifestPath: string;
|
|
11
|
+
private: boolean;
|
|
12
|
+
manifest: PackageManifest;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type PackageManifest = {
|
|
16
|
+
name: string;
|
|
17
|
+
version: string;
|
|
18
|
+
private?: boolean;
|
|
19
|
+
dependencies?: Record<string, string>;
|
|
20
|
+
devDependencies?: Record<string, string>;
|
|
21
|
+
optionalDependencies?: Record<string, string>;
|
|
22
|
+
peerDependencies?: Record<string, string>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const DEPENDENCY_FIELDS = [
|
|
26
|
+
"dependencies",
|
|
27
|
+
"devDependencies",
|
|
28
|
+
"optionalDependencies",
|
|
29
|
+
"peerDependencies",
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
export function findWorkspaceRoot(cwd: string): string {
|
|
33
|
+
const layout = detectProjectLayout(cwd);
|
|
34
|
+
const candidates = [layout.root, resolve(cwd)];
|
|
35
|
+
for (const candidate of candidates) {
|
|
36
|
+
const crates = join(candidate, "projects", "crates");
|
|
37
|
+
const packages = join(candidate, "projects", "packages");
|
|
38
|
+
if (existsSync(crates) && existsSync(packages)) {
|
|
39
|
+
return candidate;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
throw new Error("could not find Nifty workspace (expected projects/crates and projects/packages)");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function listWorkspacePackages(root: string): WorkspacePackage[] {
|
|
46
|
+
const packagesDir = join(root, "projects", "packages");
|
|
47
|
+
if (!existsSync(packagesDir)) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const packages: WorkspacePackage[] = [];
|
|
52
|
+
for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
|
|
53
|
+
if (!entry.isDirectory() || entry.name === "node_modules") {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const manifestPath = join(packagesDir, entry.name, "package.json");
|
|
57
|
+
if (!existsSync(manifestPath)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as PackageManifest;
|
|
61
|
+
if (!manifest.name || !manifest.version) {
|
|
62
|
+
throw new Error(`package.json missing name or version: ${manifestPath}`);
|
|
63
|
+
}
|
|
64
|
+
packages.push({
|
|
65
|
+
name: manifest.name,
|
|
66
|
+
version: manifest.version,
|
|
67
|
+
dir: join(packagesDir, entry.name),
|
|
68
|
+
manifestPath,
|
|
69
|
+
private: manifest.private === true,
|
|
70
|
+
manifest,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return packages.sort((a, b) => a.name.localeCompare(b.name));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function collectInternalDependencyNames(
|
|
78
|
+
pkg: WorkspacePackage,
|
|
79
|
+
byName: Map<string, WorkspacePackage>,
|
|
80
|
+
): Set<string> {
|
|
81
|
+
const deps = new Set<string>();
|
|
82
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
83
|
+
const entries = pkg.manifest[field];
|
|
84
|
+
if (!entries) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
for (const [depName, spec] of Object.entries(entries)) {
|
|
88
|
+
if (byName.has(depName)) {
|
|
89
|
+
deps.add(depName);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const resolved = resolveWorkspaceDependencyName(spec, pkg.dir, byName);
|
|
93
|
+
if (resolved) {
|
|
94
|
+
deps.add(resolved);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return deps;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveWorkspaceDependencyName(
|
|
102
|
+
spec: string,
|
|
103
|
+
packageDir: string,
|
|
104
|
+
byName: Map<string, WorkspacePackage>,
|
|
105
|
+
): string | undefined {
|
|
106
|
+
if (spec.startsWith("workspace:")) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
if (!spec.startsWith("file:")) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
const targetDir = resolve(packageDir, spec.slice("file:".length));
|
|
113
|
+
for (const candidate of byName.values()) {
|
|
114
|
+
if (candidate.dir === targetDir) {
|
|
115
|
+
return candidate.name;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const manifestPath = join(targetDir, "package.json");
|
|
119
|
+
if (!existsSync(manifestPath)) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as PackageManifest;
|
|
123
|
+
return manifest.name;
|
|
124
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { NiftyConfig, NiftyConfigEnv } from "./types.js";
|
|
2
|
-
|
|
3
|
-
/** Type-safe helper for `nifty.config.ts` / `nifty.config.js`. */
|
|
4
|
-
export function defineConfig(config: NiftyConfig): NiftyConfig;
|
|
5
|
-
export function defineConfig(
|
|
6
|
-
config: (env: NiftyConfigEnv) => NiftyConfig | Promise<NiftyConfig>,
|
|
7
|
-
): (env: NiftyConfigEnv) => NiftyConfig | Promise<NiftyConfig>;
|
|
8
|
-
export function defineConfig(
|
|
9
|
-
config: NiftyConfig | ((env: NiftyConfigEnv) => NiftyConfig | Promise<NiftyConfig>),
|
|
10
|
-
): NiftyConfig | ((env: NiftyConfigEnv) => NiftyConfig | Promise<NiftyConfig>) {
|
|
11
|
-
return config;
|
|
12
|
-
}
|
|
1
|
+
import type { NiftyConfig, NiftyConfigEnv } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/** Type-safe helper for `nifty.config.ts` / `nifty.config.js`. */
|
|
4
|
+
export function defineConfig(config: NiftyConfig): NiftyConfig;
|
|
5
|
+
export function defineConfig(
|
|
6
|
+
config: (env: NiftyConfigEnv) => NiftyConfig | Promise<NiftyConfig>,
|
|
7
|
+
): (env: NiftyConfigEnv) => NiftyConfig | Promise<NiftyConfig>;
|
|
8
|
+
export function defineConfig(
|
|
9
|
+
config: NiftyConfig | ((env: NiftyConfigEnv) => NiftyConfig | Promise<NiftyConfig>),
|
|
10
|
+
): NiftyConfig | ((env: NiftyConfigEnv) => NiftyConfig | Promise<NiftyConfig>) {
|
|
11
|
+
return config;
|
|
12
|
+
}
|
|
@@ -1,189 +1,189 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
|
|
4
|
-
export type ProjectKind = "cargo" | "npm" | "hybrid" | "unknown";
|
|
5
|
-
|
|
6
|
-
/** Detected cargo + npm project layout. */
|
|
7
|
-
export type ProjectLayout = {
|
|
8
|
-
/** Resolved project root (git root when present). */
|
|
9
|
-
root: string;
|
|
10
|
-
kind: ProjectKind;
|
|
11
|
-
/** Nearest `Cargo.toml` walking upward. */
|
|
12
|
-
cargoManifest?: string;
|
|
13
|
-
/** Nearest `package.json` walking upward (skips `node_modules`). */
|
|
14
|
-
packageManifest?: string;
|
|
15
|
-
/** Directory whose `Cargo.toml` declares a `[workspace]`. */
|
|
16
|
-
cargoWorkspaceRoot?: string;
|
|
17
|
-
/** Directory whose `package.json` declares npm `workspaces`. */
|
|
18
|
-
npmWorkspaceRoot?: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const CARGO_MANIFEST = "Cargo.toml";
|
|
22
|
-
const PACKAGE_MANIFEST = "package.json";
|
|
23
|
-
const GIT_DIR = ".git";
|
|
24
|
-
|
|
25
|
-
function isInsideNodeModules(path: string): boolean {
|
|
26
|
-
return path.split(/[/\\]/).includes("node_modules");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function findFileUpward(cwd: string, fileName: string, skipNodeModules: boolean): string | undefined {
|
|
30
|
-
let here = resolve(cwd);
|
|
31
|
-
while (true) {
|
|
32
|
-
if (skipNodeModules && isInsideNodeModules(here)) {
|
|
33
|
-
const parent = dirname(here);
|
|
34
|
-
if (parent === here) {
|
|
35
|
-
return undefined;
|
|
36
|
-
}
|
|
37
|
-
here = parent;
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
const candidate = join(here, fileName);
|
|
41
|
-
if (existsSync(candidate)) {
|
|
42
|
-
return candidate;
|
|
43
|
-
}
|
|
44
|
-
const parent = dirname(here);
|
|
45
|
-
if (parent === here) {
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
here = parent;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function findGitRoot(cwd: string): string | undefined {
|
|
53
|
-
let here = resolve(cwd);
|
|
54
|
-
while (true) {
|
|
55
|
-
if (existsSync(join(here, GIT_DIR))) {
|
|
56
|
-
return here;
|
|
57
|
-
}
|
|
58
|
-
const parent = dirname(here);
|
|
59
|
-
if (parent === here) {
|
|
60
|
-
return undefined;
|
|
61
|
-
}
|
|
62
|
-
here = parent;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function manifestHasCargoWorkspace(manifestPath: string): boolean {
|
|
67
|
-
try {
|
|
68
|
-
return readFileSync(manifestPath, "utf8")
|
|
69
|
-
.split(/\r?\n/)
|
|
70
|
-
.some((line) => line.trim() === "[workspace]");
|
|
71
|
-
} catch {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function manifestHasNpmWorkspaces(manifestPath: string): boolean {
|
|
77
|
-
try {
|
|
78
|
-
return readFileSync(manifestPath, "utf8").includes('"workspaces"');
|
|
79
|
-
} catch {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function findCargoWorkspaceRoot(manifestPath: string): string | undefined {
|
|
85
|
-
let here = dirname(resolve(manifestPath));
|
|
86
|
-
while (true) {
|
|
87
|
-
const candidate = join(here, CARGO_MANIFEST);
|
|
88
|
-
if (existsSync(candidate) && manifestHasCargoWorkspace(candidate)) {
|
|
89
|
-
return here;
|
|
90
|
-
}
|
|
91
|
-
const parent = dirname(here);
|
|
92
|
-
if (parent === here) {
|
|
93
|
-
return undefined;
|
|
94
|
-
}
|
|
95
|
-
here = parent;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function findNpmWorkspaceRoot(manifestPath: string): string | undefined {
|
|
100
|
-
let here = dirname(resolve(manifestPath));
|
|
101
|
-
while (true) {
|
|
102
|
-
const candidate = join(here, PACKAGE_MANIFEST);
|
|
103
|
-
if (existsSync(candidate) && manifestHasNpmWorkspaces(candidate)) {
|
|
104
|
-
return here;
|
|
105
|
-
}
|
|
106
|
-
const parent = dirname(here);
|
|
107
|
-
if (parent === here) {
|
|
108
|
-
return undefined;
|
|
109
|
-
}
|
|
110
|
-
here = parent;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function classifyProject(hasCargo: boolean, hasNpm: boolean): ProjectKind {
|
|
115
|
-
if (hasCargo && hasNpm) {
|
|
116
|
-
return "hybrid";
|
|
117
|
-
}
|
|
118
|
-
if (hasCargo) {
|
|
119
|
-
return "cargo";
|
|
120
|
-
}
|
|
121
|
-
if (hasNpm) {
|
|
122
|
-
return "npm";
|
|
123
|
-
}
|
|
124
|
-
return "unknown";
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Walk upward and return the nearest `Cargo.toml`. */
|
|
128
|
-
export function findCargoManifest(cwd = process.cwd()): string | undefined {
|
|
129
|
-
return findFileUpward(cwd, CARGO_MANIFEST, false);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** Walk upward and return the nearest `package.json` (skips `node_modules`). */
|
|
133
|
-
export function findPackageManifest(cwd = process.cwd()): string | undefined {
|
|
134
|
-
return findFileUpward(cwd, PACKAGE_MANIFEST, true);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Detect cargo/npm/hybrid layout from `cwd`. */
|
|
138
|
-
export function detectProjectLayout(cwd = process.cwd()): ProjectLayout {
|
|
139
|
-
const start = resolve(cwd);
|
|
140
|
-
const cargoManifest = findCargoManifest(start);
|
|
141
|
-
const packageManifest = findPackageManifest(start);
|
|
142
|
-
const cargoWorkspaceRoot = cargoManifest ? findCargoWorkspaceRoot(cargoManifest) : undefined;
|
|
143
|
-
const npmWorkspaceRoot = packageManifest ? findNpmWorkspaceRoot(packageManifest) : undefined;
|
|
144
|
-
const kind = classifyProject(Boolean(cargoManifest), Boolean(packageManifest));
|
|
145
|
-
const root =
|
|
146
|
-
findGitRoot(start) ??
|
|
147
|
-
cargoWorkspaceRoot ??
|
|
148
|
-
npmWorkspaceRoot ??
|
|
149
|
-
(cargoManifest ? dirname(cargoManifest) : undefined) ??
|
|
150
|
-
(packageManifest ? dirname(packageManifest) : undefined) ??
|
|
151
|
-
start;
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
root,
|
|
155
|
-
kind,
|
|
156
|
-
cargoManifest,
|
|
157
|
-
packageManifest,
|
|
158
|
-
cargoWorkspaceRoot,
|
|
159
|
-
npmWorkspaceRoot,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** Discover npm package directories for hybrid monorepos. */
|
|
164
|
-
export function discoverPackageDirs(layout: ProjectLayout): string[] {
|
|
165
|
-
const roots = new Set<string>();
|
|
166
|
-
|
|
167
|
-
if (layout.npmWorkspaceRoot) {
|
|
168
|
-
roots.add(layout.npmWorkspaceRoot);
|
|
169
|
-
}
|
|
170
|
-
if (layout.packageManifest) {
|
|
171
|
-
roots.add(dirname(layout.packageManifest));
|
|
172
|
-
}
|
|
173
|
-
if (layout.kind === "hybrid") {
|
|
174
|
-
const packagesDir = join(layout.root, "projects", "packages");
|
|
175
|
-
if (existsSync(packagesDir)) {
|
|
176
|
-
for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
|
|
177
|
-
if (!entry.isDirectory()) {
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
const packageJson = join(packagesDir, entry.name, "package.json");
|
|
181
|
-
if (existsSync(packageJson)) {
|
|
182
|
-
roots.add(join(packagesDir, entry.name));
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return [...roots];
|
|
189
|
-
}
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export type ProjectKind = "cargo" | "npm" | "hybrid" | "unknown";
|
|
5
|
+
|
|
6
|
+
/** Detected cargo + npm project layout. */
|
|
7
|
+
export type ProjectLayout = {
|
|
8
|
+
/** Resolved project root (git root when present). */
|
|
9
|
+
root: string;
|
|
10
|
+
kind: ProjectKind;
|
|
11
|
+
/** Nearest `Cargo.toml` walking upward. */
|
|
12
|
+
cargoManifest?: string;
|
|
13
|
+
/** Nearest `package.json` walking upward (skips `node_modules`). */
|
|
14
|
+
packageManifest?: string;
|
|
15
|
+
/** Directory whose `Cargo.toml` declares a `[workspace]`. */
|
|
16
|
+
cargoWorkspaceRoot?: string;
|
|
17
|
+
/** Directory whose `package.json` declares npm `workspaces`. */
|
|
18
|
+
npmWorkspaceRoot?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const CARGO_MANIFEST = "Cargo.toml";
|
|
22
|
+
const PACKAGE_MANIFEST = "package.json";
|
|
23
|
+
const GIT_DIR = ".git";
|
|
24
|
+
|
|
25
|
+
function isInsideNodeModules(path: string): boolean {
|
|
26
|
+
return path.split(/[/\\]/).includes("node_modules");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function findFileUpward(cwd: string, fileName: string, skipNodeModules: boolean): string | undefined {
|
|
30
|
+
let here = resolve(cwd);
|
|
31
|
+
while (true) {
|
|
32
|
+
if (skipNodeModules && isInsideNodeModules(here)) {
|
|
33
|
+
const parent = dirname(here);
|
|
34
|
+
if (parent === here) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
here = parent;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const candidate = join(here, fileName);
|
|
41
|
+
if (existsSync(candidate)) {
|
|
42
|
+
return candidate;
|
|
43
|
+
}
|
|
44
|
+
const parent = dirname(here);
|
|
45
|
+
if (parent === here) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
here = parent;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function findGitRoot(cwd: string): string | undefined {
|
|
53
|
+
let here = resolve(cwd);
|
|
54
|
+
while (true) {
|
|
55
|
+
if (existsSync(join(here, GIT_DIR))) {
|
|
56
|
+
return here;
|
|
57
|
+
}
|
|
58
|
+
const parent = dirname(here);
|
|
59
|
+
if (parent === here) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
here = parent;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function manifestHasCargoWorkspace(manifestPath: string): boolean {
|
|
67
|
+
try {
|
|
68
|
+
return readFileSync(manifestPath, "utf8")
|
|
69
|
+
.split(/\r?\n/)
|
|
70
|
+
.some((line) => line.trim() === "[workspace]");
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function manifestHasNpmWorkspaces(manifestPath: string): boolean {
|
|
77
|
+
try {
|
|
78
|
+
return readFileSync(manifestPath, "utf8").includes('"workspaces"');
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function findCargoWorkspaceRoot(manifestPath: string): string | undefined {
|
|
85
|
+
let here = dirname(resolve(manifestPath));
|
|
86
|
+
while (true) {
|
|
87
|
+
const candidate = join(here, CARGO_MANIFEST);
|
|
88
|
+
if (existsSync(candidate) && manifestHasCargoWorkspace(candidate)) {
|
|
89
|
+
return here;
|
|
90
|
+
}
|
|
91
|
+
const parent = dirname(here);
|
|
92
|
+
if (parent === here) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
here = parent;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function findNpmWorkspaceRoot(manifestPath: string): string | undefined {
|
|
100
|
+
let here = dirname(resolve(manifestPath));
|
|
101
|
+
while (true) {
|
|
102
|
+
const candidate = join(here, PACKAGE_MANIFEST);
|
|
103
|
+
if (existsSync(candidate) && manifestHasNpmWorkspaces(candidate)) {
|
|
104
|
+
return here;
|
|
105
|
+
}
|
|
106
|
+
const parent = dirname(here);
|
|
107
|
+
if (parent === here) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
here = parent;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function classifyProject(hasCargo: boolean, hasNpm: boolean): ProjectKind {
|
|
115
|
+
if (hasCargo && hasNpm) {
|
|
116
|
+
return "hybrid";
|
|
117
|
+
}
|
|
118
|
+
if (hasCargo) {
|
|
119
|
+
return "cargo";
|
|
120
|
+
}
|
|
121
|
+
if (hasNpm) {
|
|
122
|
+
return "npm";
|
|
123
|
+
}
|
|
124
|
+
return "unknown";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Walk upward and return the nearest `Cargo.toml`. */
|
|
128
|
+
export function findCargoManifest(cwd = process.cwd()): string | undefined {
|
|
129
|
+
return findFileUpward(cwd, CARGO_MANIFEST, false);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Walk upward and return the nearest `package.json` (skips `node_modules`). */
|
|
133
|
+
export function findPackageManifest(cwd = process.cwd()): string | undefined {
|
|
134
|
+
return findFileUpward(cwd, PACKAGE_MANIFEST, true);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Detect cargo/npm/hybrid layout from `cwd`. */
|
|
138
|
+
export function detectProjectLayout(cwd = process.cwd()): ProjectLayout {
|
|
139
|
+
const start = resolve(cwd);
|
|
140
|
+
const cargoManifest = findCargoManifest(start);
|
|
141
|
+
const packageManifest = findPackageManifest(start);
|
|
142
|
+
const cargoWorkspaceRoot = cargoManifest ? findCargoWorkspaceRoot(cargoManifest) : undefined;
|
|
143
|
+
const npmWorkspaceRoot = packageManifest ? findNpmWorkspaceRoot(packageManifest) : undefined;
|
|
144
|
+
const kind = classifyProject(Boolean(cargoManifest), Boolean(packageManifest));
|
|
145
|
+
const root =
|
|
146
|
+
findGitRoot(start) ??
|
|
147
|
+
cargoWorkspaceRoot ??
|
|
148
|
+
npmWorkspaceRoot ??
|
|
149
|
+
(cargoManifest ? dirname(cargoManifest) : undefined) ??
|
|
150
|
+
(packageManifest ? dirname(packageManifest) : undefined) ??
|
|
151
|
+
start;
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
root,
|
|
155
|
+
kind,
|
|
156
|
+
cargoManifest,
|
|
157
|
+
packageManifest,
|
|
158
|
+
cargoWorkspaceRoot,
|
|
159
|
+
npmWorkspaceRoot,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Discover npm package directories for hybrid monorepos. */
|
|
164
|
+
export function discoverPackageDirs(layout: ProjectLayout): string[] {
|
|
165
|
+
const roots = new Set<string>();
|
|
166
|
+
|
|
167
|
+
if (layout.npmWorkspaceRoot) {
|
|
168
|
+
roots.add(layout.npmWorkspaceRoot);
|
|
169
|
+
}
|
|
170
|
+
if (layout.packageManifest) {
|
|
171
|
+
roots.add(dirname(layout.packageManifest));
|
|
172
|
+
}
|
|
173
|
+
if (layout.kind === "hybrid") {
|
|
174
|
+
const packagesDir = join(layout.root, "projects", "packages");
|
|
175
|
+
if (existsSync(packagesDir)) {
|
|
176
|
+
for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
|
|
177
|
+
if (!entry.isDirectory()) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const packageJson = join(packagesDir, entry.name, "package.json");
|
|
181
|
+
if (existsSync(packageJson)) {
|
|
182
|
+
roots.add(join(packagesDir, entry.name));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return [...roots];
|
|
189
|
+
}
|