fix(npm-aqua-compiler): Support aquaDir inside the project's node_nodules (#427)

When aqua dir inside the project's node_nodules dir, return only the subtree based on that internal path
This commit is contained in:
Akim 2024-01-31 20:50:53 +07:00 committed by GitHub
parent fa38328fdd
commit 514663a4fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 180 additions and 69 deletions

View File

@ -14,12 +14,67 @@
* limitations under the License.
*/
import { join } from "path";
import { join, resolve } from "path";
import { fileURLToPath } from "url";
import { assert, describe, expect, it } from "vitest";
import { gatherImportsFromNpm } from "./imports.js";
import { gatherImportsFromNpm, GatherImportsResult } from "./imports.js";
const prefix = join(
fileURLToPath(new URL("./", import.meta.url)),
"..",
"test",
"transitive-deps",
"project",
);
function buildResolutionKey(str: string) {
return str
.slice(prefix.length)
.split("/node_modules/")
.filter(Boolean)
.join("/");
}
function matchTree(
expected: GatherImportsResult,
actual: GatherImportsResult,
aquaToCompileDirPath: string | undefined,
) {
if (aquaToCompileDirPath !== undefined) {
aquaToCompileDirPath = resolve(aquaToCompileDirPath);
}
expect(Object.keys(actual).length).toBe(Object.keys(expected).length);
Object.entries(actual).forEach(([key, value]) => {
const resolutionKey =
key === aquaToCompileDirPath ? key : buildResolutionKey(key);
const resolutionValues = expected[resolutionKey];
assert(resolutionValues);
expect(Object.keys(value).length).toBe(
Object.keys(resolutionValues).length,
);
for (const [dep, path] of Object.entries(value)) {
if (Array.isArray(path)) {
expect(dep).toBe("");
expect(expected[resolutionKey]).toHaveProperty(dep, path);
continue;
}
expect(expected[resolutionKey]).toHaveProperty(
dep,
buildResolutionKey(path),
);
}
});
}
describe("imports", () => {
/**
@ -35,81 +90,90 @@ describe("imports", () => {
string,
Record<string, string[] | string>
> = {
[aquaToCompileDirPath]: {
[resolve(aquaToCompileDirPath)]: {
"": globalImports,
A: "./A",
B: "./B",
A: "A",
B: "B",
},
"./A": {
C: "./C",
D: "./D",
A: {
C: "C",
D: "D",
},
"./B": {
C: "./B/C",
D: "./B/D",
B: {
C: "B/C",
D: "B/D",
},
"./C": {
D: "./C/D",
C: {
D: "C/D",
},
"./B/C": {
D: "./B/C/D",
"B/C": {
D: "B/C/D",
},
};
const prefix = join(
fileURLToPath(new URL("./", import.meta.url)),
"..",
"test",
"transitive-deps",
"project",
);
const buildResolutionKey = (str: string) => {
return (
"./" +
str
.slice(prefix.length)
.split("/node_modules/")
.filter(Boolean)
.join("/")
);
};
const imports = await gatherImportsFromNpm({
npmProjectDirPath,
aquaToCompileDirPath,
globalImports,
});
expect(Object.keys(imports).length).toBe(
Object.keys(expectedResolution).length,
);
matchTree(expectedResolution, imports, aquaToCompileDirPath);
});
Object.entries(imports).forEach(([key, value]) => {
const resolutionKey =
key === aquaToCompileDirPath ? key : buildResolutionKey(key);
it("should resolve transitive dependencies and return a subtree when 'aquaToCompileDirPath' inside project 'node_modules' folder", async () => {
const npmProjectDirPath = "./test/transitive-deps/project";
const resolutionValues = expectedResolution[resolutionKey];
const aquaToCompileDirPath =
"./test/transitive-deps/project/node_modules/A";
assert(resolutionValues);
const globalImports = ["./.fluence/aqua"];
expect(Object.keys(value).length).toBe(
Object.keys(resolutionValues).length,
);
const expectedResolution: Record<
string,
Record<string, string[] | string>
> = {
[resolve(aquaToCompileDirPath)]: {
"": globalImports,
C: "C",
D: "D",
},
C: {
D: "C/D",
},
};
for (const [dep, path] of Object.entries(value)) {
if (Array.isArray(path)) {
expect(dep).toBe("");
expect(expectedResolution[resolutionKey]).toHaveProperty(dep, path);
continue;
}
expect(expectedResolution[resolutionKey]).toHaveProperty(
dep,
buildResolutionKey(path),
);
}
const imports = await gatherImportsFromNpm({
npmProjectDirPath,
aquaToCompileDirPath,
globalImports,
});
matchTree(expectedResolution, imports, aquaToCompileDirPath);
});
it("should resolve transitive dependencies when project is empty", async () => {
const npmProjectDirPath = "./test/transitive-deps/empty-project";
const aquaToCompileDirPath =
"./test/transitive-deps/empty-project/node_modules/A";
const globalImports = ["./.fluence/aqua"];
const expectedResolution: Record<
string,
Record<string, string[] | string>
> = {
[resolve(aquaToCompileDirPath)]: {
"": globalImports,
},
};
const imports = await gatherImportsFromNpm({
npmProjectDirPath,
aquaToCompileDirPath,
globalImports,
});
matchTree(expectedResolution, imports, aquaToCompileDirPath);
});
});

View File

@ -14,6 +14,8 @@
* limitations under the License.
*/
import { resolve } from "path";
import Arborist from "@npmcli/arborist";
import { breadth } from "treeverse";
@ -40,7 +42,9 @@ export async function gatherImportsFromNpm({
* Traverse dependency tree to construct map
* (real path of a package) -> (real paths of its immediate dependencies)
*/
const result: GatherImportsResult = {};
let result: Record<string, Record<string, string>> = {};
const rootDepsKey = "";
const aquaDepPath = resolve(aquaToCompileDirPath ?? npmProjectDirPath);
breadth({
tree,
@ -64,15 +68,8 @@ export async function gatherImportsFromNpm({
// Root node should have top-level property pointed to aqua dependency folder
if (node.isRoot) {
const aquaDepPath = aquaToCompileDirPath ?? npmProjectDirPath;
result[aquaDepPath] = {
...(result[aquaDepPath] ??
(globalImports.length > 0
? {
"": globalImports,
}
: {})),
result[rootDepsKey] = {
...result[rootDepsKey],
[dep.name]: dep.realpath,
};
} else {
@ -88,5 +85,38 @@ export async function gatherImportsFromNpm({
},
});
return result;
// In case 'aquaToCompileDirPath' points to any dependency inside current project
// Only the subtree with 'aquaToCompileDirPath' as root node is returned
if (aquaToCompileDirPath !== undefined && aquaDepPath in result) {
// Other nodes which are not included in the subtree simply dropped
const newResult: Record<string, Record<string, string>> = {};
breadth({
tree: aquaDepPath,
getChildren: (node) => {
const deps = result[node];
if (deps === undefined) {
return [];
}
const isRootNode = node === aquaDepPath;
newResult[isRootNode ? rootDepsKey : node] = deps;
return Object.values(deps);
},
});
result = newResult;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [rootDepsKey]: _, ...rest } = result;
return {
...rest,
[aquaDepPath]: {
...result[rootDepsKey],
"": globalImports,
},
};
}

View File

@ -0,0 +1,12 @@
{
"name": "empty-project",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "empty-project",
"version": "0.1.0"
}
}
}

View File

@ -0,0 +1,5 @@
{
"name": "empty-project",
"version": "0.1.0",
"dependencies": {}
}