@layerzerolabs/build-utils-rust 0.2.113
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/.turbo/turbo-build.log +43 -0
- package/.turbo/turbo-lint.log +8 -0
- package/.turbo/turbo-test.log +58 -0
- package/README.md +111 -0
- package/dist/457ZJTJH.js +52 -0
- package/dist/457ZJTJH.js.map +1 -0
- package/dist/LQ26FJL3.js +112 -0
- package/dist/LQ26FJL3.js.map +1 -0
- package/dist/SPODHCGD.js +59 -0
- package/dist/SPODHCGD.js.map +1 -0
- package/dist/TOEFRNPD.js +83 -0
- package/dist/TOEFRNPD.js.map +1 -0
- package/dist/UDDAJZY4.js +26 -0
- package/dist/UDDAJZY4.js.map +1 -0
- package/dist/VUOMXK5T.js +6 -0
- package/dist/VUOMXK5T.js.map +1 -0
- package/dist/WKRDI535.js +51 -0
- package/dist/WKRDI535.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +28 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +8 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +4 -0
- package/dist/logger.js.map +1 -0
- package/dist/resolve/discover.d.ts +23 -0
- package/dist/resolve/discover.d.ts.map +1 -0
- package/dist/resolve/discover.js +7 -0
- package/dist/resolve/discover.js.map +1 -0
- package/dist/resolve/exclude.d.ts +10 -0
- package/dist/resolve/exclude.d.ts.map +1 -0
- package/dist/resolve/exclude.js +4 -0
- package/dist/resolve/exclude.js.map +1 -0
- package/dist/resolve/index.d.ts +20 -0
- package/dist/resolve/index.d.ts.map +1 -0
- package/dist/resolve/index.js +9 -0
- package/dist/resolve/index.js.map +1 -0
- package/dist/resolve/io.d.ts +17 -0
- package/dist/resolve/io.d.ts.map +1 -0
- package/dist/resolve/io.js +6 -0
- package/dist/resolve/io.js.map +1 -0
- package/dist/resolve/rewrite.d.ts +41 -0
- package/dist/resolve/rewrite.d.ts.map +1 -0
- package/dist/resolve/rewrite.js +8 -0
- package/dist/resolve/rewrite.js.map +1 -0
- package/package.json +46 -0
- package/src/cli.ts +33 -0
- package/src/index.ts +1 -0
- package/src/logger.ts +26 -0
- package/src/resolve/discover.ts +93 -0
- package/src/resolve/exclude.ts +52 -0
- package/src/resolve/index.ts +122 -0
- package/src/resolve/io.ts +69 -0
- package/src/resolve/rewrite.ts +210 -0
- package/tests/resolve/discover.test.ts +206 -0
- package/tests/resolve/fixtures/index.ts +6 -0
- package/tests/resolve/fixtures/minimal.toml +2 -0
- package/tests/resolve/fixtures/mixed-deps.toml +4 -0
- package/tests/resolve/fixtures/multi-path-deps.toml +9 -0
- package/tests/resolve/fixtures/multi-section-path-deps.toml +13 -0
- package/tests/resolve/fixtures/no-path-deps.toml +3 -0
- package/tests/resolve/fixtures/oapp-with-stale-deps.toml +5 -0
- package/tests/resolve/fixtures/patch-multi-registry.toml +22 -0
- package/tests/resolve/fixtures/program-with-deps.toml +5 -0
- package/tests/resolve/fixtures/program-with-macros.toml +6 -0
- package/tests/resolve/fixtures/program-workspace-member.toml +5 -0
- package/tests/resolve/fixtures/root-only-sections.toml +13 -0
- package/tests/resolve/fixtures/stellar-consumer.toml +8 -0
- package/tests/resolve/fixtures/stellar-member-inheriting.toml +10 -0
- package/tests/resolve/fixtures/stellar-workspace-root.toml +17 -0
- package/tests/resolve/fixtures/stellar-workspace-with-tests.toml +15 -0
- package/tests/resolve/fixtures/target-cfg-path-dep.toml +10 -0
- package/tests/resolve/fixtures/with-features.toml +2 -0
- package/tests/resolve/fixtures/workspace-at-end.toml +6 -0
- package/tests/resolve/fixtures/workspace-at-top.toml +10 -0
- package/tests/resolve/fixtures/workspace-middle.toml +10 -0
- package/tests/resolve/fixtures/workspace-with-default-members.toml +8 -0
- package/tests/resolve/fixtures/workspace-with-path-deps.toml +8 -0
- package/tests/resolve/index.test.ts +298 -0
- package/tests/resolve/rewrite.test.ts +420 -0
- package/tsconfig.json +21 -0
- package/tsup.config.ts +8 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendor @layerzerolabs Cargo dependencies from node_modules into a flat
|
|
3
|
+
* dependencies/ directory with rewritten path deps, so a Rust package builds
|
|
4
|
+
* without the rest of the monorepo.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { copyFile, mkdir, rm } from 'fs/promises';
|
|
8
|
+
import { join, relative, resolve } from 'path';
|
|
9
|
+
|
|
10
|
+
import { logger } from '../logger';
|
|
11
|
+
import { discoverPackages, isCrate, type VendorTarget } from './discover';
|
|
12
|
+
import type { CollectedTree } from './io';
|
|
13
|
+
import { collectFiles } from './io';
|
|
14
|
+
import type { RewriteContext } from './rewrite';
|
|
15
|
+
import { rewriteCargoTomls } from './rewrite';
|
|
16
|
+
|
|
17
|
+
// ─── Path Map ───
|
|
18
|
+
|
|
19
|
+
/** Maps each crate's npm package source path to its vendored dependencies/ location. */
|
|
20
|
+
export const buildDependenciesPathMap = (targets: VendorTarget[]): Map<string, string> => {
|
|
21
|
+
const pathMap = new Map<string, string>();
|
|
22
|
+
|
|
23
|
+
for (const { npmPkgDir, depsPkgDir, tree } of targets) {
|
|
24
|
+
for (const dir of tree.manifestDirs) {
|
|
25
|
+
pathMap.set(join(npmPkgDir, dir), join(depsPkgDir, dir));
|
|
26
|
+
logger.trace(`map ${join(npmPkgDir, dir)} -> ${join(depsPkgDir, dir)}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
logger.debug(`built dependency path map: ${pathMap.size} entries`);
|
|
30
|
+
return pathMap;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// ─── File Operations ───
|
|
34
|
+
|
|
35
|
+
/** Copy a collected tree into destRoot. */
|
|
36
|
+
const copyCollected = async (
|
|
37
|
+
srcRoot: string,
|
|
38
|
+
destRoot: string,
|
|
39
|
+
tree: CollectedTree,
|
|
40
|
+
): Promise<void> => {
|
|
41
|
+
await mkdir(destRoot, { recursive: true });
|
|
42
|
+
await Promise.all(tree.dirs.map((dir) => mkdir(join(destRoot, dir), { recursive: true })));
|
|
43
|
+
await Promise.all(
|
|
44
|
+
tree.files.map((file) => copyFile(join(srcRoot, file), join(destRoot, file))),
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Resolves the Cargo root: `cargoDir` if provided, else `packageRoot`. Throws if no Cargo.toml. */
|
|
49
|
+
const resolveCargoRoot = async (packageRoot: string, cargoDir?: string): Promise<string> => {
|
|
50
|
+
if (cargoDir) {
|
|
51
|
+
const resolved = resolve(packageRoot, cargoDir);
|
|
52
|
+
if (!(await isCrate(resolved))) {
|
|
53
|
+
throw new Error(`--cargo-dir ${cargoDir} does not contain a Cargo.toml`);
|
|
54
|
+
}
|
|
55
|
+
return resolved;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!(await isCrate(packageRoot))) {
|
|
59
|
+
throw new Error(`${packageRoot} does not contain a Cargo.toml`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return packageRoot;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ─── Public API ───
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Entry point: vendor every @layerzerolabs Cargo crate reachable through node_modules
|
|
69
|
+
* into dependencies/, with path deps rewritten to resolve locally.
|
|
70
|
+
*
|
|
71
|
+
* @param cwd pnpm package root (where node_modules/ lives). Default: process.cwd()
|
|
72
|
+
* @param cargoDir Cargo crate directory to vendor into. Default: packageRoot
|
|
73
|
+
*/
|
|
74
|
+
export const resolveDependencies = async ({
|
|
75
|
+
cwd,
|
|
76
|
+
cargoDir,
|
|
77
|
+
}: { cwd?: string; cargoDir?: string } = {}): Promise<void> => {
|
|
78
|
+
const packageRoot = resolve(cwd ?? process.cwd());
|
|
79
|
+
|
|
80
|
+
const depsRoot = join(await resolveCargoRoot(packageRoot, cargoDir), 'dependencies');
|
|
81
|
+
|
|
82
|
+
const discovered = new Map<string, string>();
|
|
83
|
+
await discoverPackages(packageRoot, discovered, new Set());
|
|
84
|
+
|
|
85
|
+
if (discovered.size === 0) {
|
|
86
|
+
logger.info('No @layerzerolabs namespaced Cargo dependencies found in node_modules');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Rebuild dependencies/ from scratch each run.
|
|
91
|
+
await rm(depsRoot, { recursive: true, force: true });
|
|
92
|
+
await mkdir(depsRoot, { recursive: true });
|
|
93
|
+
|
|
94
|
+
// Collect each package's tree once — copy, path map, and rewrite all reuse it.
|
|
95
|
+
const targets: VendorTarget[] = await Promise.all(
|
|
96
|
+
Array.from(discovered, async ([pkgName, npmPkgDir]) => {
|
|
97
|
+
const tree = await collectFiles(npmPkgDir);
|
|
98
|
+
logger.debug(
|
|
99
|
+
`${pkgName}: collected ${tree.files.length} files, ${tree.dirs.length} dirs`,
|
|
100
|
+
);
|
|
101
|
+
return { pkgName, npmPkgDir, depsPkgDir: join(depsRoot, pkgName), tree };
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const ctx: RewriteContext = {
|
|
106
|
+
pathMap: buildDependenciesPathMap(targets),
|
|
107
|
+
discovered,
|
|
108
|
+
};
|
|
109
|
+
logger.debug(`path map: ${ctx.pathMap.size} entries`);
|
|
110
|
+
|
|
111
|
+
await Promise.all(
|
|
112
|
+
targets.map(async ({ pkgName, npmPkgDir, depsPkgDir, tree }) => {
|
|
113
|
+
await copyCollected(npmPkgDir, depsPkgDir, tree);
|
|
114
|
+
await rewriteCargoTomls(npmPkgDir, depsPkgDir, tree, ctx);
|
|
115
|
+
logger.debug(` ${pkgName} -> ${npmPkgDir}`);
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
logger.info(
|
|
120
|
+
`Resolved ${discovered.size} Cargo dependencies into ${relative(packageRoot, depsRoot) || './dependencies/'}`,
|
|
121
|
+
);
|
|
122
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { readdir, realpath, stat } from 'fs/promises';
|
|
2
|
+
import { basename, dirname, extname, join } from 'path';
|
|
3
|
+
|
|
4
|
+
import { logger } from '../logger';
|
|
5
|
+
import { COPY_EXTENSIONS, removeExcludedFiles } from './exclude';
|
|
6
|
+
|
|
7
|
+
export const tryRealpath = async (path: string): Promise<string | undefined> => {
|
|
8
|
+
try {
|
|
9
|
+
return await realpath(path);
|
|
10
|
+
} catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const tryIsDirectory = async (path: string): Promise<boolean> => {
|
|
16
|
+
try {
|
|
17
|
+
return (await stat(path)).isDirectory();
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const tryReaddir = async (dir: string): Promise<string[]> => {
|
|
24
|
+
try {
|
|
25
|
+
return await readdir(dir);
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export interface CollectedTree {
|
|
32
|
+
/** directories containing copyable content, relative to root (no empties) */
|
|
33
|
+
dirs: string[];
|
|
34
|
+
/** copyable files (COPY_EXTENSIONS), relative to root (excluded subtrees pruned) */
|
|
35
|
+
files: string[];
|
|
36
|
+
/** dirs that directly contain a Cargo.toml (crate roots), relative to root; '.' is the root */
|
|
37
|
+
manifestDirs: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Walk `root` once, applying exclusion per level and keeping only COPY_EXTENSIONS files.
|
|
42
|
+
* Also records `manifestDirs` (dirs holding a Cargo.toml) so consumers don't re-scan.
|
|
43
|
+
*/
|
|
44
|
+
export const collectFiles = async (root: string, relativePath = ''): Promise<CollectedTree> => {
|
|
45
|
+
const dirs: string[] = [];
|
|
46
|
+
const files: string[] = [];
|
|
47
|
+
const manifestDirs: string[] = [];
|
|
48
|
+
|
|
49
|
+
const rawFileNames = await tryReaddir(join(root, relativePath));
|
|
50
|
+
const fileNames = removeExcludedFiles(rawFileNames).map((e) => join(relativePath, e));
|
|
51
|
+
|
|
52
|
+
for (const file of fileNames) {
|
|
53
|
+
if (await tryIsDirectory(join(root, file))) {
|
|
54
|
+
const subDir = await collectFiles(root, file);
|
|
55
|
+
if (subDir.files.length > 0) {
|
|
56
|
+
dirs.push(file, ...subDir.dirs);
|
|
57
|
+
files.push(...subDir.files);
|
|
58
|
+
manifestDirs.push(...subDir.manifestDirs);
|
|
59
|
+
} else {
|
|
60
|
+
logger.trace(`skip empty dir ${file}`);
|
|
61
|
+
}
|
|
62
|
+
} else if (COPY_EXTENSIONS.has(extname(file))) {
|
|
63
|
+
files.push(file);
|
|
64
|
+
if (basename(file) === 'Cargo.toml') manifestDirs.push(dirname(file));
|
|
65
|
+
logger.trace(`collect ${file}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { dirs, files, manifestDirs };
|
|
69
|
+
};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrite a vendored Cargo.toml's path deps into the flat dependencies/ layout,
|
|
3
|
+
* operating on the parsed TOML (via smol-toml) rather than text.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
7
|
+
import { dirname, join, relative } from 'path';
|
|
8
|
+
import { parse, stringify } from 'smol-toml';
|
|
9
|
+
|
|
10
|
+
import { logger } from '../logger';
|
|
11
|
+
import { isWorkspaceRoot } from './discover';
|
|
12
|
+
import { hasExcludedSegment } from './exclude';
|
|
13
|
+
import { type CollectedTree, tryRealpath } from './io';
|
|
14
|
+
|
|
15
|
+
/** Bundled resolution state passed through the rewrite pipeline. */
|
|
16
|
+
export interface RewriteContext {
|
|
17
|
+
pathMap: Map<string, string>;
|
|
18
|
+
discovered: Map<string, string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Table = Record<string, unknown>;
|
|
22
|
+
|
|
23
|
+
/** A dependency spec that carries a `path` — an inline table like `{ path = "…" }`. */
|
|
24
|
+
type PathDepSpec = Table & { path: string };
|
|
25
|
+
|
|
26
|
+
const isTable = (value: unknown): value is Table =>
|
|
27
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
28
|
+
|
|
29
|
+
const DEP_TABLE_KEYS = ['dependencies', 'dev-dependencies', 'build-dependencies'] as const;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Dependency specs that declare a `path`, across every dependency table (plus
|
|
33
|
+
* `[workspace.dependencies]` when `includeWorkspaceDeps`). Specs are live references
|
|
34
|
+
* into `manifest`, so mutating `.path` updates it.
|
|
35
|
+
*/
|
|
36
|
+
const pathDepSpecs = (manifest: Table, includeWorkspaceDeps: boolean): PathDepSpec[] => {
|
|
37
|
+
// The root manifest and each [target.'cfg(...)'] table both hold DEP_TABLE_KEYS tables.
|
|
38
|
+
const containers = [
|
|
39
|
+
manifest,
|
|
40
|
+
...(isTable(manifest.target) ? Object.values(manifest.target).filter(isTable) : []),
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const depTables = [
|
|
44
|
+
...containers.flatMap((container) => DEP_TABLE_KEYS.map((key) => container[key])),
|
|
45
|
+
...(includeWorkspaceDeps && isTable(manifest.workspace)
|
|
46
|
+
? [manifest.workspace.dependencies]
|
|
47
|
+
: []),
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
return depTables
|
|
51
|
+
.filter(isTable)
|
|
52
|
+
.flatMap((table) => Object.values(table))
|
|
53
|
+
.filter((spec): spec is PathDepSpec => isTable(spec) && typeof spec.path === 'string');
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Drop literal `members` / `default-members` entries whose path contains an excluded
|
|
58
|
+
* directory segment (e.g. `tests/mock_vault`) — those dirs are never vendored, so the
|
|
59
|
+
* member would dangle and fail Cargo's workspace load. Globs (`tests/*`) are kept; Cargo
|
|
60
|
+
* re-expands them to a harmless empty match. Returns true if any member was removed.
|
|
61
|
+
*/
|
|
62
|
+
export const pruneExcludedLiteralMembers = (manifest: Table): boolean => {
|
|
63
|
+
const workspace = manifest.workspace;
|
|
64
|
+
if (!isTable(workspace)) return false;
|
|
65
|
+
|
|
66
|
+
let changed = false;
|
|
67
|
+
for (const key of ['members', 'default-members'] as const) {
|
|
68
|
+
const members = workspace[key];
|
|
69
|
+
if (!Array.isArray(members)) continue;
|
|
70
|
+
|
|
71
|
+
// Keep everything except literal (non-glob) members inside an excluded dir.
|
|
72
|
+
const kept = members.filter(
|
|
73
|
+
(m) => typeof m !== 'string' || m.includes('*') || !hasExcludedSegment(m),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (kept.length !== members.length) {
|
|
77
|
+
workspace[key] = kept;
|
|
78
|
+
changed = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return changed;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** Rebase every path dep in `manifest` into the flat dependencies/ layout. Returns true if any changed. */
|
|
85
|
+
export const rewritePathDeps = async (
|
|
86
|
+
manifest: Table,
|
|
87
|
+
originalDir: string,
|
|
88
|
+
manifestDir: string,
|
|
89
|
+
ctx: RewriteContext,
|
|
90
|
+
includeWorkspaceDeps: boolean,
|
|
91
|
+
): Promise<boolean> => {
|
|
92
|
+
let changed = false;
|
|
93
|
+
for (const spec of pathDepSpecs(manifest, includeWorkspaceDeps)) {
|
|
94
|
+
const target =
|
|
95
|
+
(await resolveViaRealpath(originalDir, spec.path, ctx.pathMap)) ??
|
|
96
|
+
resolveViaNameFallback(spec.path, ctx);
|
|
97
|
+
if (!target) continue;
|
|
98
|
+
|
|
99
|
+
const rewritten = relative(manifestDir, target);
|
|
100
|
+
if (rewritten === spec.path) continue;
|
|
101
|
+
|
|
102
|
+
logger.trace(`rewrite path dep ${spec.path} -> ${rewritten}`);
|
|
103
|
+
spec.path = rewritten;
|
|
104
|
+
changed = true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return changed;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Delete the `[workspace]` / `[patch.*]` / `[profile.*]` tables — invalid or ignored in a
|
|
112
|
+
* vendored dependency (only honored at the real workspace root). Returns true if any was removed.
|
|
113
|
+
*/
|
|
114
|
+
export const stripRootOnlyTables = (manifest: Table): boolean => {
|
|
115
|
+
let changed = false;
|
|
116
|
+
for (const table of ['workspace', 'patch', 'profile'] as const) {
|
|
117
|
+
if (manifest[table] != null) {
|
|
118
|
+
delete manifest[table];
|
|
119
|
+
changed = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return changed;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Rewrite one vendored Cargo.toml in place: rebase its path deps, then strip the root-only
|
|
127
|
+
* tables — or, for a preserved workspace root (`stripRootTables = false`), prune excluded
|
|
128
|
+
* members instead. Re-serialized only if something changed.
|
|
129
|
+
*
|
|
130
|
+
* @param originalDir source crate dir the manifest was copied from
|
|
131
|
+
* @param cargoTomlPath the vendored Cargo.toml to rewrite
|
|
132
|
+
*/
|
|
133
|
+
export const rewriteCargoToml = async (
|
|
134
|
+
originalDir: string,
|
|
135
|
+
cargoTomlPath: string,
|
|
136
|
+
ctx: RewriteContext,
|
|
137
|
+
stripRootTables = true,
|
|
138
|
+
): Promise<void> => {
|
|
139
|
+
const manifest = parse(await readFile(cargoTomlPath, 'utf-8'));
|
|
140
|
+
|
|
141
|
+
const pathsChanged = await rewritePathDeps(
|
|
142
|
+
manifest,
|
|
143
|
+
originalDir,
|
|
144
|
+
dirname(cargoTomlPath),
|
|
145
|
+
ctx,
|
|
146
|
+
!stripRootTables,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const tablesChanged = stripRootTables
|
|
150
|
+
? stripRootOnlyTables(manifest)
|
|
151
|
+
: pruneExcludedLiteralMembers(manifest);
|
|
152
|
+
|
|
153
|
+
if (pathsChanged || tablesChanged) {
|
|
154
|
+
await writeFile(cargoTomlPath, stringify(manifest));
|
|
155
|
+
logger.debug(`rewrote ${cargoTomlPath}`);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/** Primary resolution: realpath the path through pnpm symlinks, then look it up in pathMap. */
|
|
160
|
+
const resolveViaRealpath = async (
|
|
161
|
+
originalDir: string,
|
|
162
|
+
pathValue: string,
|
|
163
|
+
pathMap: Map<string, string>,
|
|
164
|
+
): Promise<string | undefined> => {
|
|
165
|
+
const resolvedPath = await tryRealpath(join(originalDir, pathValue));
|
|
166
|
+
return resolvedPath ? pathMap.get(resolvedPath) : undefined;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/** Fallback: match the path's leaf dir name against a discovered package (for already-`dependencies/…` paths). */
|
|
170
|
+
const resolveViaNameFallback = (pathValue: string, ctx: RewriteContext): string | undefined => {
|
|
171
|
+
const segments = pathValue.split('/').filter(Boolean);
|
|
172
|
+
const leafDir = segments.at(-1);
|
|
173
|
+
if (!leafDir) return undefined;
|
|
174
|
+
|
|
175
|
+
for (const [pkgName, realPath] of ctx.discovered) {
|
|
176
|
+
// Direct match: "dependencies/utils-solana-rbac" → match "utils-solana-rbac"
|
|
177
|
+
if (pkgName === leafDir) {
|
|
178
|
+
return ctx.pathMap.get(realPath);
|
|
179
|
+
}
|
|
180
|
+
// Subdir match: "dependencies/utils-solana-rbac/macros" → match parent
|
|
181
|
+
const parentDir = segments.at(-2);
|
|
182
|
+
if (parentDir && pkgName === parentDir) {
|
|
183
|
+
const subTarget = ctx.pathMap.get(join(realPath, leafDir));
|
|
184
|
+
if (subTarget) return subTarget;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return undefined;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Rewrite every Cargo.toml in a copied dependency tree. The root manifest keeps its
|
|
192
|
+
* root-only tables when it's a workspace (members inherit from them); all others strip them.
|
|
193
|
+
*/
|
|
194
|
+
export const rewriteCargoTomls = async (
|
|
195
|
+
srcRoot: string,
|
|
196
|
+
destRoot: string,
|
|
197
|
+
tree: CollectedTree,
|
|
198
|
+
ctx: RewriteContext,
|
|
199
|
+
): Promise<void> => {
|
|
200
|
+
for (const manifestDir of tree.manifestDirs) {
|
|
201
|
+
const preserve = manifestDir === '.' && (await isWorkspaceRoot(destRoot));
|
|
202
|
+
|
|
203
|
+
await rewriteCargoToml(
|
|
204
|
+
join(srcRoot, manifestDir),
|
|
205
|
+
join(destRoot, manifestDir, 'Cargo.toml'),
|
|
206
|
+
ctx,
|
|
207
|
+
!preserve,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { mkdirSync, rmSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { buildDependenciesPathMap } from '../../src/resolve';
|
|
6
|
+
import {
|
|
7
|
+
discoverPackages,
|
|
8
|
+
isCrate,
|
|
9
|
+
isWorkspaceRoot,
|
|
10
|
+
type VendorTarget,
|
|
11
|
+
} from '../../src/resolve/discover';
|
|
12
|
+
import { collectFiles } from '../../src/resolve/io';
|
|
13
|
+
|
|
14
|
+
const TMP = join(__dirname, '__tmp_discover__');
|
|
15
|
+
|
|
16
|
+
const createFile = (path: string, content = '') => {
|
|
17
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
18
|
+
writeFileSync(path, content);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const createCrate = (path: string, cargoContent = '[package]\nname = "test"') => {
|
|
22
|
+
mkdirSync(path, { recursive: true });
|
|
23
|
+
writeFileSync(join(path, 'Cargo.toml'), cargoContent);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
beforeEach(() => mkdirSync(TMP, { recursive: true }));
|
|
27
|
+
afterEach(() => rmSync(TMP, { recursive: true, force: true }));
|
|
28
|
+
|
|
29
|
+
describe('isCrate', () => {
|
|
30
|
+
it('returns true for directory with Cargo.toml', async () => {
|
|
31
|
+
createCrate(join(TMP, 'my-crate'));
|
|
32
|
+
expect(await isCrate(join(TMP, 'my-crate'))).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns false for directory without Cargo.toml', async () => {
|
|
36
|
+
mkdirSync(join(TMP, 'not-a-crate'), { recursive: true });
|
|
37
|
+
expect(await isCrate(join(TMP, 'not-a-crate'))).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns false for nonexistent path', async () => {
|
|
41
|
+
expect(await isCrate(join(TMP, 'nope'))).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns false for a file', async () => {
|
|
45
|
+
createFile(join(TMP, 'a-file'), 'hello');
|
|
46
|
+
expect(await isCrate(join(TMP, 'a-file'))).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('discoverPackages', () => {
|
|
51
|
+
it('discovers @layerzerolabs packages with Cargo.toml from node_modules', async () => {
|
|
52
|
+
// Simulate: TMP/node_modules/@layerzerolabs/my-crate/Cargo.toml
|
|
53
|
+
const cratePath = join(TMP, 'node_modules', '@layerzerolabs', 'my-crate');
|
|
54
|
+
createCrate(cratePath);
|
|
55
|
+
|
|
56
|
+
const discovered = new Map<string, string>();
|
|
57
|
+
await discoverPackages(TMP, discovered, new Set());
|
|
58
|
+
|
|
59
|
+
expect(discovered.size).toBe(1);
|
|
60
|
+
expect(discovered.has('my-crate')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('skips packages without Cargo.toml', async () => {
|
|
64
|
+
const pkgPath = join(TMP, 'node_modules', '@layerzerolabs', 'ts-only-pkg');
|
|
65
|
+
mkdirSync(pkgPath, { recursive: true });
|
|
66
|
+
writeFileSync(join(pkgPath, 'package.json'), '{}');
|
|
67
|
+
|
|
68
|
+
const discovered = new Map<string, string>();
|
|
69
|
+
await discoverPackages(TMP, discovered, new Set());
|
|
70
|
+
|
|
71
|
+
expect(discovered.size).toBe(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('discovers transitive deps by recursing into package node_modules', async () => {
|
|
75
|
+
// Root depends on pkg-a, pkg-a depends on pkg-b
|
|
76
|
+
const pkgA = join(TMP, 'node_modules', '@layerzerolabs', 'pkg-a');
|
|
77
|
+
const pkgB = join(pkgA, 'node_modules', '@layerzerolabs', 'pkg-b');
|
|
78
|
+
createCrate(pkgA);
|
|
79
|
+
createCrate(pkgB);
|
|
80
|
+
|
|
81
|
+
const discovered = new Map<string, string>();
|
|
82
|
+
await discoverPackages(TMP, discovered, new Set());
|
|
83
|
+
|
|
84
|
+
expect(discovered.size).toBe(2);
|
|
85
|
+
expect(discovered.has('pkg-a')).toBe(true);
|
|
86
|
+
expect(discovered.has('pkg-b')).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('deduplicates packages seen through multiple paths', async () => {
|
|
90
|
+
// Both pkg-a and pkg-b depend on shared (via symlinks in real pnpm,
|
|
91
|
+
// but here we just put it in both node_modules)
|
|
92
|
+
const pkgA = join(TMP, 'node_modules', '@layerzerolabs', 'pkg-a');
|
|
93
|
+
const shared = join(TMP, 'node_modules', '@layerzerolabs', 'shared');
|
|
94
|
+
const sharedViaA = join(pkgA, 'node_modules', '@layerzerolabs', 'shared');
|
|
95
|
+
createCrate(pkgA);
|
|
96
|
+
createCrate(shared);
|
|
97
|
+
createCrate(sharedViaA);
|
|
98
|
+
|
|
99
|
+
const discovered = new Map<string, string>();
|
|
100
|
+
await discoverPackages(TMP, discovered, new Set());
|
|
101
|
+
|
|
102
|
+
// "shared" discovered once from root, not duplicated from pkg-a's copy
|
|
103
|
+
expect(discovered.size).toBe(2); // pkg-a + shared
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('isWorkspaceRoot', () => {
|
|
108
|
+
it('returns true when Cargo.toml has a [workspace] table', async () => {
|
|
109
|
+
createCrate(join(TMP, 'ws'), '[workspace]\nmembers = ["contracts/*"]');
|
|
110
|
+
expect(await isWorkspaceRoot(join(TMP, 'ws'))).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('returns true for a virtual manifest ([workspace] without [package])', async () => {
|
|
114
|
+
createCrate(join(TMP, 'virtual'), '[workspace]\nresolver = "2"\nmembers = ["a"]');
|
|
115
|
+
expect(await isWorkspaceRoot(join(TMP, 'virtual'))).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('returns false for a bare [package] (standalone single crate)', async () => {
|
|
119
|
+
createCrate(join(TMP, 'leaf'), '[package]\nname = "leaf"');
|
|
120
|
+
expect(await isWorkspaceRoot(join(TMP, 'leaf'))).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('returns false when there is no Cargo.toml', async () => {
|
|
124
|
+
mkdirSync(join(TMP, 'empty'), { recursive: true });
|
|
125
|
+
expect(await isWorkspaceRoot(join(TMP, 'empty'))).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('buildDependenciesPathMap', () => {
|
|
130
|
+
// Build a VendorTarget by collecting the crate's tree (same input the pipeline feeds it).
|
|
131
|
+
const toTarget = async (
|
|
132
|
+
pkgName: string,
|
|
133
|
+
npmPkgDir: string,
|
|
134
|
+
depsDir: string,
|
|
135
|
+
): Promise<VendorTarget> => ({
|
|
136
|
+
pkgName,
|
|
137
|
+
npmPkgDir,
|
|
138
|
+
depsPkgDir: join(depsDir, pkgName),
|
|
139
|
+
tree: await collectFiles(npmPkgDir),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('maps discovered packages to dependencies/ targets', async () => {
|
|
143
|
+
const depsDir = join(TMP, 'dependencies');
|
|
144
|
+
// A discovered package always has a root Cargo.toml (isCrate-verified), so its
|
|
145
|
+
// '.' manifest dir maps the package root to its dependencies/ target.
|
|
146
|
+
const rbac = join(TMP, 'rbac');
|
|
147
|
+
const oapp = join(TMP, 'oapp');
|
|
148
|
+
createCrate(rbac);
|
|
149
|
+
createCrate(oapp);
|
|
150
|
+
const targets = [
|
|
151
|
+
await toTarget('utils-solana-rbac', rbac, depsDir),
|
|
152
|
+
await toTarget('oapp-solana-impl', oapp, depsDir),
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const pathMap = buildDependenciesPathMap(targets);
|
|
156
|
+
|
|
157
|
+
expect(pathMap.get(rbac)).toBe(join(depsDir, 'utils-solana-rbac'));
|
|
158
|
+
expect(pathMap.get(oapp)).toBe(join(depsDir, 'oapp-solana-impl'));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('includes subdirectories with Cargo.toml (macros/)', async () => {
|
|
162
|
+
const depsDir = join(TMP, 'dependencies');
|
|
163
|
+
const crateDir = join(TMP, 'real-crate');
|
|
164
|
+
createCrate(crateDir);
|
|
165
|
+
createCrate(join(crateDir, 'macros'));
|
|
166
|
+
|
|
167
|
+
const pathMap = buildDependenciesPathMap([await toTarget('my-crate', crateDir, depsDir)]);
|
|
168
|
+
|
|
169
|
+
expect(pathMap.get(crateDir)).toBe(join(depsDir, 'my-crate'));
|
|
170
|
+
expect(pathMap.get(join(crateDir, 'macros'))).toBe(join(depsDir, 'my-crate', 'macros'));
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('skips excluded directories like node_modules and target', async () => {
|
|
174
|
+
const depsDir = join(TMP, 'dependencies');
|
|
175
|
+
const crateDir = join(TMP, 'real-crate');
|
|
176
|
+
createCrate(crateDir);
|
|
177
|
+
createCrate(join(crateDir, 'node_modules')); // should be excluded
|
|
178
|
+
createCrate(join(crateDir, 'target')); // should be excluded
|
|
179
|
+
|
|
180
|
+
const pathMap = buildDependenciesPathMap([await toTarget('my-crate', crateDir, depsDir)]);
|
|
181
|
+
|
|
182
|
+
expect(pathMap.size).toBe(1); // only the root, no subdirs
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('maps every crate dir (incl. deeply nested) to its dependencies/ target', async () => {
|
|
186
|
+
const depsDir = join(TMP, 'dependencies');
|
|
187
|
+
const crateDir = join(TMP, 'protocol-stellar');
|
|
188
|
+
createCrate(crateDir, '[workspace]\nmembers = ["contracts/*", "contracts/oapps/*"]');
|
|
189
|
+
createCrate(join(crateDir, 'contracts', 'utils'));
|
|
190
|
+
createCrate(join(crateDir, 'contracts', 'oapps'));
|
|
191
|
+
createCrate(join(crateDir, 'contracts', 'oapps', 'oft-core'));
|
|
192
|
+
|
|
193
|
+
const pathMap = buildDependenciesPathMap([
|
|
194
|
+
await toTarget('protocol-stellar', crateDir, depsDir),
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
expect(pathMap.get(crateDir)).toBe(join(depsDir, 'protocol-stellar'));
|
|
198
|
+
expect(pathMap.get(join(crateDir, 'contracts', 'utils'))).toBe(
|
|
199
|
+
join(depsDir, 'protocol-stellar', 'contracts', 'utils'),
|
|
200
|
+
);
|
|
201
|
+
// the deep member — the case the old one-level scan missed
|
|
202
|
+
expect(pathMap.get(join(crateDir, 'contracts', 'oapps', 'oft-core'))).toBe(
|
|
203
|
+
join(depsDir, 'protocol-stellar', 'contracts', 'oapps', 'oft-core'),
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "my-crate"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
[dependencies]
|
|
7
|
+
anchor-lang = { version = "0.32.1" }
|
|
8
|
+
|
|
9
|
+
[dev-dependencies]
|
|
10
|
+
test-utils = { path = "dependencies/test-utils" }
|
|
11
|
+
|
|
12
|
+
[build-dependencies]
|
|
13
|
+
codegen = { path = "dependencies/codegen-utils" }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "my-crate"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
[dependencies]
|
|
7
|
+
anchor-lang = { version = "0.32.1" }
|
|
8
|
+
|
|
9
|
+
[patch.crates-io]
|
|
10
|
+
anchor-lang = { git = "https://github.com/example/anchor.git", rev = "deadbeef" }
|
|
11
|
+
|
|
12
|
+
[patch."https://github.com/solana-labs/solana.git"]
|
|
13
|
+
solana-program = { path = "../solana-program" }
|
|
14
|
+
|
|
15
|
+
[profile.dev]
|
|
16
|
+
opt-level = 1
|
|
17
|
+
|
|
18
|
+
[profile.dev.package."*"]
|
|
19
|
+
opt-level = 3
|
|
20
|
+
|
|
21
|
+
[profile.release]
|
|
22
|
+
overflow-checks = true
|